Merge branch 'patch-18' of https://github.com/gjones2/Freeside
authorIvan Kohler <ivan@freeside.biz>
Thu, 25 Apr 2013 11:15:41 +0000 (04:15 -0700)
committerIvan Kohler <ivan@freeside.biz>
Thu, 25 Apr 2013 11:15:41 +0000 (04:15 -0700)
409 files changed:
FS/FS.pm
FS/FS/AccessRight.pm
FS/FS/ClientAPI/Bulk.pm [deleted file]
FS/FS/ClientAPI/MyAccount.pm
FS/FS/ClientAPI/Signup.pm
FS/FS/ClientAPI_XMLRPC.pm
FS/FS/Conf.pm
FS/FS/Cron/bill.pm
FS/FS/Cron/upload.pm
FS/FS/L10N/en_us.pm
FS/FS/Mason.pm
FS/FS/Misc.pm
FS/FS/Misc/DateTime.pm
FS/FS/Misc/Geo.pm
FS/FS/Record.pm
FS/FS/Report/FCC_477.pm
FS/FS/Schema.pm
FS/FS/TemplateItem_Mixin.pm
FS/FS/Template_Mixin.pm
FS/FS/UI/Web.pm
FS/FS/Upgrade.pm
FS/FS/access_right.pm
FS/FS/cdr.pm
FS/FS/cdr/asterisk_skip_clid.pm [new file with mode: 0644]
FS/FS/cdr/gsm_tap3_12.pm [new file with mode: 0644]
FS/FS/cdr/huawei_softx3000.pm [new file with mode: 0644]
FS/FS/cdr/taqua62.pm
FS/FS/cdr/telstra.pm
FS/FS/cdr/u4.pm [new file with mode: 0644]
FS/FS/cdr_cust_pkg_usage.pm [new file with mode: 0644]
FS/FS/contact.pm
FS/FS/contact_Mixin.pm [new file with mode: 0644]
FS/FS/cust_bill.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_bill_pkg_tax_location.pm
FS/FS/cust_credit.pm
FS/FS/cust_credit_bill_pkg.pm
FS/FS/cust_location.pm
FS/FS/cust_main.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_main/Location.pm
FS/FS/cust_main/Packages.pm
FS/FS/cust_main/Search.pm
FS/FS/cust_main/_Marketgear.pm [deleted file]
FS/FS/cust_main_county.pm
FS/FS/cust_pay.pm
FS/FS/cust_pay_batch.pm
FS/FS/cust_pkg.pm
FS/FS/cust_pkg_discount.pm
FS/FS/cust_pkg_usage.pm [new file with mode: 0644]
FS/FS/cust_svc.pm
FS/FS/export_svc.pm
FS/FS/msg_template.pm
FS/FS/part_event/Action/Mixin/credit_agent_pkg_class.pm
FS/FS/part_event/Action/Mixin/credit_pkg.pm
FS/FS/part_event/Action/cust_bill_send_reminder.pm
FS/FS/part_event/Action/referral_pkg_billdate.pm [new file with mode: 0644]
FS/FS/part_event/Action/referral_pkg_discount.pm [new file with mode: 0644]
FS/FS/part_event/Condition/cust_bill_owed_percent.pm [new file with mode: 0644]
FS/FS/part_event/Condition/has_pkgpart.pm
FS/FS/part_event/Condition/has_referral_custnum.pm
FS/FS/part_event/Condition/message_email.pm [new file with mode: 0644]
FS/FS/part_event/Condition/once_percust.pm
FS/FS/part_event/Condition/once_perinv.pm
FS/FS/part_event/Condition/times_percust.pm [new file with mode: 0644]
FS/FS/part_export.pm
FS/FS/part_export/acct_xmlrpc.pm
FS/FS/part_export/dma_radiusmanager.pm [deleted file]
FS/FS/part_export/fibernetics_did.pm
FS/FS/part_export/http_status.pm
FS/FS/part_export/huawei_hlr.pm [new file with mode: 0644]
FS/FS/part_export/netsapiens.pm
FS/FS/part_export/phone_shellcommands.pm
FS/FS/part_export/shellcommands.pm
FS/FS/part_export/sqlradius.pm
FS/FS/part_export/status_shellcommands.pm
FS/FS/part_export/test.pm [new file with mode: 0644]
FS/FS/part_pkg.pm
FS/FS/part_pkg/base_delayed.pm [deleted file]
FS/FS/part_pkg/base_rate.pm [deleted file]
FS/FS/part_pkg/delayed_Mixin.pm
FS/FS/part_pkg/flat_introrate.pm
FS/FS/part_pkg/prorate_Mixin.pm
FS/FS/part_pkg/voip_cdr.pm
FS/FS/part_pkg/voip_inbound.pm
FS/FS/part_pkg_link.pm
FS/FS/part_pkg_msgcat.pm [new file with mode: 0644]
FS/FS/part_pkg_usage.pm [new file with mode: 0644]
FS/FS/part_pkg_usage_class.pm [new file with mode: 0644]
FS/FS/part_svc.pm
FS/FS/part_svc_column.pm
FS/FS/pay_batch.pm
FS/FS/pay_batch/BoM.pm
FS/FS/pay_batch/eft_canada.pm
FS/FS/pay_batch/nacha.pm [new file with mode: 0644]
FS/FS/pay_batch/paymentech.pm
FS/FS/payinfo_transaction_Mixin.pm
FS/FS/prospect_main.pm
FS/FS/quotation.pm
FS/FS/quotation_pkg.pm
FS/FS/rate.pm
FS/FS/rate_region.pm
FS/FS/svc_Common.pm
FS/FS/svc_Tower_Mixin.pm
FS/FS/svc_acct.pm
FS/FS/svc_broadband.pm
FS/FS/svc_export_machine.pm
FS/FS/svc_hardware.pm
FS/FS/svc_pbx.pm
FS/FS/svc_phone.pm
FS/MANIFEST
FS/bin/freeside-cdr-sftp_and_import
FS/bin/freeside-cdrrated
FS/bin/freeside-cdrrewrited
FS/bin/freeside-ipifony-download
FS/bin/freeside-phonenum_list [new file with mode: 0755]
FS/bin/freeside-queued
FS/bin/freeside-selfservice-server
FS/bin/freeside-upgrade
FS/bin/freeside-username_list [new file with mode: 0755]
FS/bin/freeside-wkhtmltopdf
FS/t/cdr_cust_pkg_usage.t [new file with mode: 0644]
FS/t/contact_Mixin.t [new file with mode: 0644]
FS/t/cust_pkg_usage.t [new file with mode: 0644]
FS/t/part_pkg_msgcat.t [new file with mode: 0644]
FS/t/part_pkg_usage.t [new file with mode: 0644]
FS/t/part_pkg_usage_class.t [new file with mode: 0644]
INSTALL [deleted file]
Makefile
bin/23diff
bin/cdr-netsapiens.import
bin/cust_main-bulk_change
bin/fs-migrate-supplemental [new file with mode: 0755]
bin/megapop.pl [new file with mode: 0755]
conf/invoice_html
conf/invoice_htmlsummary
conf/invoice_latex
conf/invoice_latexsummary
debian/OLD/config [new file with mode: 0644]
debian/OLD/cron.d [new file with mode: 0644]
debian/OLD/dbconfig-common.install [new file with mode: 0644]
debian/OLD/dbconfig-common.upgrade [new file with mode: 0644]
debian/OLD/freeside.apache-alias.conf [new file with mode: 0644]
debian/OLD/postinst [new file with mode: 0644]
debian/OLD/postrm [new file with mode: 0644]
debian/OLD/prerm [new file with mode: 0644]
debian/TODO
debian/changelog
debian/compat
debian/config [deleted file]
debian/control
debian/copyright
debian/cron.d [deleted file]
debian/dbconfig-common.install [deleted file]
debian/dbconfig-common.upgrade [deleted file]
debian/freeside.apache-alias.conf [deleted file]
debian/freeside.docs
debian/init.d.ex [deleted file]
debian/init.d.lsb.ex [deleted file]
debian/postinst [deleted file]
debian/postrm [deleted file]
debian/prerm [deleted file]
debian/rules
debian/templates [deleted file]
etc/megapop.pl [deleted file]
fs_selfservice/FS-SelfService/SelfService.pm
fs_selfservice/FS-SelfService/cgi/myaccount_menu.html
fs_selfservice/FS-SelfService/cgi/selfservice.cgi
fs_selfservice/FS-SelfService/cgi/signup.html
fs_selfservice/FS-SelfService/cgi/small_custview.html
fs_selfservice/FS-SelfService/cgi/view_cdr_details.html
fs_selfservice/FS-SelfService/cgi/view_usage.html
htetc/freeside-rt.conf
httemplate/browse/msgcat.html
httemplate/browse/part_export.cgi
httemplate/browse/part_pkg.cgi
httemplate/browse/part_pkg_usage.html [new file with mode: 0644]
httemplate/browse/part_svc.cgi
httemplate/browse/rate_region.html
httemplate/docs/license.html
httemplate/edit/REAL_cust_pkg.cgi
httemplate/edit/bulk-part_pkg.html [new file with mode: 0644]
httemplate/edit/credit-cust_bill_pkg.html
httemplate/edit/cust_location.cgi
httemplate/edit/cust_main.cgi
httemplate/edit/cust_main/billing.html
httemplate/edit/cust_main/bottomfixup.js
httemplate/edit/cust_main/choose_tax_location.html [deleted file]
httemplate/edit/cust_main/top_misc.html
httemplate/edit/cust_pkg.cgi
httemplate/edit/cust_pkg_detail.html
httemplate/edit/cust_pkg_quantity.html [new file with mode: 0755]
httemplate/edit/cust_refund.cgi
httemplate/edit/elements/edit.html
httemplate/edit/elements/part_svc_column.html [new file with mode: 0644]
httemplate/edit/elements/svc_Common.html
httemplate/edit/part_export.cgi
httemplate/edit/part_pkg.cgi
httemplate/edit/part_svc.cgi
httemplate/edit/part_tag.html
httemplate/edit/payment_gateway.html
httemplate/edit/phone_device.html
httemplate/edit/process/REAL_cust_pkg.cgi
httemplate/edit/process/bulk-part_pkg.html [new file with mode: 0644]
httemplate/edit/process/change-cust_pkg.html
httemplate/edit/process/credit-cust_bill_pkg.html
httemplate/edit/process/cust_location.cgi
httemplate/edit/process/cust_main.cgi
httemplate/edit/process/cust_pkg_quantity.html [new file with mode: 0644]
httemplate/edit/process/cust_svc.cgi
httemplate/edit/process/elements/process.html
httemplate/edit/process/elements/svc_Common.html
httemplate/edit/process/part_export.cgi
httemplate/edit/process/part_pkg.cgi
httemplate/edit/process/part_pkg_usage.html [new file with mode: 0644]
httemplate/edit/process/quick-cust_pkg.cgi
httemplate/edit/process/svc_phone.html
httemplate/edit/quick-charge.html
httemplate/edit/rate_region.cgi
httemplate/edit/svc_acct.cgi
httemplate/edit/svc_broadband.cgi
httemplate/edit/svc_phone.cgi
httemplate/elements/auto-table.html
httemplate/elements/change_history_common.html
httemplate/elements/change_password.html [new file with mode: 0644]
httemplate/elements/checkbox-tristate.html [new file with mode: 0644]
httemplate/elements/contact.html
httemplate/elements/dashboard-toplist.html
httemplate/elements/fckeditor/fckeditor.js
httemplate/elements/location.html
httemplate/elements/menu.html
httemplate/elements/order_pkg.js
httemplate/elements/progress-init.html
httemplate/elements/random_pass.html [new file with mode: 0644]
httemplate/elements/search-svc_broadband.html [new file with mode: 0644]
httemplate/elements/select-areacode.html
httemplate/elements/select-did.html
httemplate/elements/select-exchange.html
httemplate/elements/select-mac.html
httemplate/elements/select-part_svc.html
httemplate/elements/select-phonenum.html
httemplate/elements/select-region.html
httemplate/elements/select-table.html
httemplate/elements/select-tiered.html
httemplate/elements/selectlayers.html
httemplate/elements/standardize_locations.js
httemplate/elements/tr-cust_svc.html
httemplate/elements/tr-input-beginning_ending.html
httemplate/elements/tr-search-svc_broadband.html [new file with mode: 0644]
httemplate/elements/tr-select-contact.html [new file with mode: 0644]
httemplate/elements/tr-select-cust_location.html
httemplate/elements/tr-select-did.html
httemplate/elements/tr-select-discount_term.html
httemplate/elements/tr-select-from_to.html
httemplate/elements/tr-select-inventory_item.html [new file with mode: 0644]
httemplate/elements/tr-select-part_svc.html
httemplate/elements/tr-select-reason.html
httemplate/elements/tr-select-voip_class.html
httemplate/misc/areacodes.cgi
httemplate/misc/batch-cust_pay.html
httemplate/misc/change_pkg.cgi
httemplate/misc/change_pkg_contact.html [new file with mode: 0755]
httemplate/misc/choose_tax_location.html
httemplate/misc/confirm-address_standardize.html
httemplate/misc/confirm-cust_pkg-edit_dates.html [new file with mode: 0755]
httemplate/misc/cust-part_pkg.cgi
httemplate/misc/email-customers.html
httemplate/misc/exchanges.cgi
httemplate/misc/location.cgi
httemplate/misc/macinventory.cgi
httemplate/misc/maestro-customer_status.html
httemplate/misc/manage_cust_email.html [new file with mode: 0644]
httemplate/misc/order_pkg.html
httemplate/misc/part_export/huawei_hlr-import_sim.html [new file with mode: 0644]
httemplate/misc/part_export/process/huawei_hlr-import_sim.html [new file with mode: 0644]
httemplate/misc/part_svc-columns.cgi
httemplate/misc/phonenums.cgi
httemplate/misc/process/change-password.html [new file with mode: 0644]
httemplate/misc/process/change_pkg_contact.html [new file with mode: 0644]
httemplate/misc/process/manage_cust_email.html [new file with mode: 0644]
httemplate/misc/process/payment.cgi
httemplate/misc/regions.cgi
httemplate/misc/xmlhttp-address_standardize.html
httemplate/misc/xmlhttp-calculate_taxes.html
httemplate/misc/xmlhttp-cust_bill-search.html
httemplate/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html
httemplate/misc/xmlhttp-cust_main-censustract.html
httemplate/misc/xmlhttp-cust_main-discount_terms.cgi
httemplate/misc/xmlhttp-cust_main-email_search.html [new file with mode: 0644]
httemplate/misc/xmlhttp-cust_main-search.cgi
httemplate/misc/xmlhttp-ping.html
httemplate/misc/xmlhttp-svc_broadband-search.cgi [new file with mode: 0644]
httemplate/pref/pref-process.html
httemplate/pref/pref.html
httemplate/search/477.html
httemplate/search/477partIA.html [new file with mode: 0755]
httemplate/search/477partIA_detail.html [deleted file]
httemplate/search/477partIA_summary.html [deleted file]
httemplate/search/477partIIA.html
httemplate/search/477partIIB.html
httemplate/search/477partV.html
httemplate/search/477partVI_census.html
httemplate/search/agent_commission.html [new file with mode: 0644]
httemplate/search/agent_inventory.html
httemplate/search/bill_batch.cgi
httemplate/search/cdr.html
httemplate/search/cust_bill.html
httemplate/search/cust_bill_event.cgi
httemplate/search/cust_bill_pay.html
httemplate/search/cust_bill_pkg.cgi
httemplate/search/cust_bill_pkg_discount.html
httemplate/search/cust_bill_pkg_referral.html
httemplate/search/cust_credit.html
httemplate/search/cust_credit_bill.html
httemplate/search/cust_credit_bill_pkg.html
httemplate/search/cust_credit_refund.html
httemplate/search/cust_event.html
httemplate/search/cust_main-zip.html
httemplate/search/cust_main.cgi
httemplate/search/cust_main.html
httemplate/search/cust_pay_batch.cgi
httemplate/search/cust_pkg.cgi
httemplate/search/cust_pkg_discount.html
httemplate/search/cust_pkg_svc.html
httemplate/search/cust_svc.html
httemplate/search/cust_tax_adjustment.html
httemplate/search/cust_tax_exempt.cgi
httemplate/search/cust_tax_exempt_pkg.cgi
httemplate/search/customer_accounting_summary.html
httemplate/search/elements/checkbox-foot.html [new file with mode: 0644]
httemplate/search/elements/cust_main_dayranges.html
httemplate/search/elements/cust_pay_batch_top.html
httemplate/search/elements/cust_pay_or_refund.html
httemplate/search/elements/report_cust_pay_or_refund.html
httemplate/search/elements/report_svc_Common.html [new file with mode: 0644]
httemplate/search/elements/search-html.html
httemplate/search/elements/search.html
httemplate/search/elements/svc_Common.html [new file with mode: 0644]
httemplate/search/employee_audit.html
httemplate/search/inventory_item.html
httemplate/search/mailinglistmember.html
httemplate/search/part_pkg.html
httemplate/search/pay_batch.cgi
httemplate/search/phone_avail.html
httemplate/search/phone_inventory_provisioned.html
httemplate/search/prepaid_income.html
httemplate/search/prepay_credit.html
httemplate/search/prospect_main.html
httemplate/search/qual.cgi
httemplate/search/queue.html
httemplate/search/quotation.html
httemplate/search/reg_code.html
httemplate/search/report_477.html
httemplate/search/report_agent_commission.html [new file with mode: 0644]
httemplate/search/report_cust_bill.html
httemplate/search/report_cust_main.html
httemplate/search/report_employee_audit.html
httemplate/search/report_employee_commission.html
httemplate/search/report_receivables.html
httemplate/search/report_sqlradius_usage.html
httemplate/search/report_svc_acct.html
httemplate/search/report_svc_phone.html
httemplate/search/report_svc_phone_usage.html [new file with mode: 0644]
httemplate/search/report_tax.cgi
httemplate/search/report_tax.html
httemplate/search/rt_ticket.html
httemplate/search/rt_transaction.html
httemplate/search/sql.html
httemplate/search/sqlradius.cgi
httemplate/search/sqlradius.html
httemplate/search/svc_acct.cgi
httemplate/search/svc_broadband.cgi
httemplate/search/svc_dish.cgi
httemplate/search/svc_domain.cgi
httemplate/search/svc_external.cgi
httemplate/search/svc_forward.cgi
httemplate/search/svc_hardware.cgi
httemplate/search/svc_phone.cgi
httemplate/search/svc_www.cgi
httemplate/search/timeworked.html
httemplate/search/unearned_detail.html
httemplate/search/unprovisioned_services.html
httemplate/view/bill_batch.cgi
httemplate/view/cust_main/billing.html
httemplate/view/cust_main/change_history.html
httemplate/view/cust_main/packages.html
httemplate/view/cust_main/packages/contact.html [new file with mode: 0644]
httemplate/view/cust_main/packages/location.html
httemplate/view/cust_main/packages/package.html
httemplate/view/cust_main/packages/section.html
httemplate/view/cust_main/packages/status.html
httemplate/view/cust_main/payment_history.html
httemplate/view/cust_main/payment_history/attempted_batch_payment.html [new file with mode: 0644]
httemplate/view/elements/svc_Common.html
httemplate/view/elements/svc_devices.html
httemplate/view/elements/svc_edit_link.html
httemplate/view/elements/svc_export_status.html
httemplate/view/quotation-pdf.cgi [new file with mode: 0755]
httemplate/view/svc_acct.cgi
httemplate/view/svc_acct/basics.html
httemplate/view/svc_broadband.cgi
httemplate/view/svc_cert.cgi
httemplate/view/svc_hardware.cgi
httemplate/view/svc_phone.cgi
rt/FREESIDE_MODIFIED
rt/lib/RT/Action/SendEmail.pm
rt/lib/RT/CustomField.pm
rt/share/html/Admin/CustomFields/Modify.html
rt/share/html/Ticket/Create.html

index 2d963b5..d8bc333 100644 (file)
--- a/FS/FS.pm
+++ b/FS/FS.pm
@@ -3,7 +3,7 @@ package FS;
 use strict;
 use vars qw($VERSION);
 
-$VERSION = '3.0git';
+$VERSION = '3.1git';
 
 #find missing entries in this file with:
 # for a in `ls *pm | cut -d. -f1`; do grep 'L<FS::'$a'>' ../FS.pm >/dev/null || echo "missing $a" ; done
@@ -231,6 +231,8 @@ L<FS::pkg_class> - Package class class
 
 L<FS::part_pkg> - Package definition class
 
+L<FS::part_pkg_msgcat> - Package definition localization class
+
 L<FS::part_pkg_link> - Package definition link class
 
 L<FS::part_pkg_taxclass> - Tax class class
index 66624e1..bfb39b4 100644 (file)
@@ -162,6 +162,7 @@ tie my %rights, 'Tie::IxHash',
     'Recharge customer service', #NEW
     'Unprovision customer service',
     'Change customer service', #NEWNEW
+    'Edit password',
     'Edit usage', #NEW
     'Edit home dir', #NEW
     'Edit www config', #NEW
@@ -182,6 +183,7 @@ tie my %rights, 'Tie::IxHash',
     'Unvoid invoices',
     'Delete invoices',
     'View customer tax exemptions', #yow
+    'Edit customer tax exemptions', #NEWNEW
     'Add customer tax adjustment', #new, but no need to phase in
     'View customer batched payments', #NEW
     'View customer pending payments', #NEW
@@ -212,6 +214,7 @@ tie my %rights, 'Tie::IxHash',
   ###
   'Customer credit and refund rights' => [
     'Post credit',
+    'Credit line items', #NEWNEWNEW
     'Apply credit', #NEWNEW
     { rightname=>'Unapply credit', desc=>'Enable "unapplication" of unclosed credits.' }, #aka unapplycredits
     { rightname=>'Delete credit', desc=>'Enable deletion of unclosed credits. Be very careful!  Only delete credits that were data-entry errors, not adjustments.' }, #aka. deletecredits Optionally specify one or more comma-separated email addresses to be notified when a credit is deleted.
@@ -293,6 +296,7 @@ tie my %rights, 'Tie::IxHash',
     'Services: Hardware',
     'Services: Hardware: Advanced search',
     'Services: Phone numbers',
+    'Services: Phone numbers: Advanced search',
     'Services: PBXs',
     'Services: Ports',
     'Services: Mailing lists',
@@ -301,6 +305,8 @@ tie my %rights, 'Tie::IxHash',
     'Usage: Call Detail Records (CDRs)',
     'Usage: Unrateable CDRs',
     'Usage: Time worked',
+    { rightname=>'Employees: Commission Report', global=>1 },
+    { rightname=>'Employees: Audit Report', global=>1 },
 
     #{ rightname => 'List customers of all agents', global=>1 },
   ],
@@ -339,6 +345,8 @@ tie my %rights, 'Tie::IxHash',
     'Edit package definitions',
     { rightname=>'Edit global package definitions', global=>1 },
   
+    'Bulk edit package definitions',
+
     'Edit billing events',
     { rightname=>'Edit global billing events', global=>1 },
 
diff --git a/FS/FS/ClientAPI/Bulk.pm b/FS/FS/ClientAPI/Bulk.pm
deleted file mode 100644 (file)
index ec617df..0000000
+++ /dev/null
@@ -1,384 +0,0 @@
-package FS::ClientAPI::Bulk;
-
-use strict;
-
-use vars qw( $DEBUG $cache );
-use Date::Parse;
-use FS::Record qw( qsearchs );
-use FS::Conf;
-use FS::ClientAPI_SessionCache;
-use FS::cust_main;
-use FS::cust_pkg;
-use FS::cust_svc;
-use FS::svc_acct;
-use FS::svc_external;
-use FS::cust_recon;
-use Data::Dumper;
-
-$DEBUG = 1;
-
-sub _cache {
-  $cache ||= new FS::ClientAPI_SessionCache ( {
-               'namespace' => 'FS::ClientAPI::Agent', #yes, share session_ids
-             } );
-}
-
-sub _izoom_ftp_row_fixup {
-  my $hash = shift;
-
-  my @addr_fields = qw( address1 address2 city state zip );
-  my @fields = ( qw( agent_custid username _password first last ),
-                 @addr_fields,
-                 map { "ship_$_" } @addr_fields );
-
-  $hash->{$_} =~ s/[&\/\*'"]/_/g foreach @fields;
-
-  #$hash->{action} = '' if $hash->{action} eq 'R'; #unsupported for ftp
-
-  $hash->{refnum} = 1;  #ahem
-  $hash->{country} = 'US';
-  $hash->{ship_country} = 'US';
-  $hash->{payby} = 'LECB';
-  $hash->{payinfo} = $hash->{daytime};
-  $hash->{ship_fax} = '' if ( !$hash->{sms} ||  $hash->{sms} eq 'F' );
-
-  my $has_ship =
-    grep { $hash->{"ship_$_"} &&
-           (! $hash->{$_} || $hash->{"ship_$_"} ne $hash->{$_} )
-         }
-    ( @addr_fields, 'fax' );
-
-  if ( $has_ship )  {
-    foreach ( @addr_fields, qw( first last ) ) {
-      $hash->{"ship_$_"} = $hash->{$_} unless $hash->{"ship_$_"};
-    }
-  }
-    
-  delete $hash->{sms};
-
-  '';
-
-};
-
-sub _izoom_ftp_result {
-  my ($hash, $error) = @_;
-  my $cust_main =
-      qsearchs( 'cust_main', { 'agent_custid' => $hash->{agent_custid},
-                               'agentnum'     => $hash->{agentnum}
-                             }
-              );
-
-  my $custnum = $cust_main ? $cust_main->custnum : '';
-  my @response = ( $hash->{action}, $hash->{agent_custid}, $custnum );
-
-  if ( $error ) {
-    push @response, ( 'ERROR', $error );
-  } else {
-    push @response, ( 'OK', 'OK' );
-  }
-
-  join( ',', @response );
-
-}
-
-sub _izoom_ftp_badaction {
-  "Invalid action: $_[0] record: @_ ";
-}
-
-sub _izoom_soap_row_fixup { _izoom_ftp_row_fixup(@_) };
-
-sub _izoom_soap_result {
-  my ($hash, $error) = @_;
-
-  if ( $hash->{action} eq 'R' ) {
-    if ( $error ) {
-      return "Please check errors:\n $error"; # odd extra space
-    } else {
-      return join(' ', "Everything ok.", $hash->{pkg}, $hash->{adjourn} );
-    }
-  }
-
-  my $pkg = $hash->{pkg} || $hash->{saved_pkg} || '';
-  if ( $error ) {
-    return join(' ', $hash->{agent_custid}, $error );
-  } else {
-    return join(' ', $hash->{agent_custid}, $pkg, $hash->{adjourn} );
-  }
-
-}
-
-sub _izoom_soap_badaction {
-  "Unknown action '$_[13]' ";
-}
-
-my %format = (
-  'izoom-ftp'  => {
-                    'fields' => [ qw ( action agent_custid username _password
-                                       daytime ship_fax sms first last
-                                       address1 address2 city state zip
-                                       pkg adjourn ship_address1 ship_address2
-                                       ship_city ship_state ship_zip ) ],
-                    'fixup'  =>  sub { _izoom_ftp_row_fixup(@_) },
-                    'result' =>  sub { _izoom_ftp_result(@_) },
-                    'action' =>  sub { _izoom_ftp_badaction(@_) },
-                  },
-  'izoom-soap' => {
-                    'fields' => [ qw ( agent_custid username _password
-                                       daytime first last address1 address2
-                                       city state zip pkg action adjourn
-                                       ship_fax sms ship_address1 ship_address2
-                                       ship_city ship_state ship_zip ) ],
-                    'fixup'  =>  sub { _izoom_soap_row_fixup(@_) },
-                    'result' =>  sub { _izoom_soap_result(@_) },
-                    'action' =>  sub { _izoom_soap_badaction(@_) },
-                  },
-);
-
-sub processrow {
-  my $p = shift;
-
-  my $session = _cache->get($p->{'session_id'})
-    or return { 'error' => "Can't resume session" }; #better error message
-
-  my $conf = new FS::Conf;
-  my $format = $conf->config('selfservice-bulk_format', $session->{agentnum})
-               || 'izoom-soap';
-  my ( @row ) = @{ $p->{row} };
-
-  warn "processrow called with '". join("' '", @row). "'\n" if $DEBUG;
-
-  return { 'error' => "unknown format: $format" }
-    unless exists $format{$format};
-
-  return { 'error' => "Invalid record record length: ". scalar(@row).
-                      "record: @row " #sic
-         }
-    unless scalar(@row) == scalar(@{$format{$format}{fields}});
-
-  my %hash = ( 'agentnum' => $session->{agentnum} );
-  my $error;
-
-  foreach my $field ( @{ $format{ $format }{ fields } } ) {
-    $hash{$field} = shift @row;
-  }
-
-  $error ||= &{ $format{ $format }{ fixup } }( \%hash );
-  
-  # put in the fixup routine?
-  if ( 'R' eq $hash{action} ) {
-    warn "processing reconciliation\n" if $DEBUG;
-    $error ||= process_recon($hash{agentnum}, $hash{agent_custid});
-  } elsif ( 'P' eq $hash{action} ) {
-    #  do nothing
-  } elsif( 'D' eq $hash{action} ) {
-    $hash{promo_pkg} = 'disk-1-'. $session->{agent};
-  } elsif ( 'S' eq $hash{action} ) {
-    $hash{promo_pkg} = 'disk-2-'. $session->{agent};
-    $hash{saved_pkg} = $hash{pkg};
-    $hash{pkg} = '';
-  } else {
-    $error ||= &{ $format{ $format }{ action } }( @row );
-  }
-
-  warn "processing provision\n" if ($DEBUG && !$error && $hash{action} ne 'R');
-  $error ||= provision( %hash ) unless $hash{action} eq 'R';
-
-  my $result =  &{ $format{ $format }{ result } }( \%hash, $error );
-
-  warn "processrow returning '". join("' '", $result, $error). "'\n"
-    if $DEBUG;
-
-  return { 'error' => $error, 'message' => $result };
-
-}
-
-sub provision {
-  my %args = ( @_ );
-
-  delete $args{action};
-
-  my $cust_main =
-    qsearchs( 'cust_main',
-              { map { $_ => $args{$_} } qw ( agent_custid agentnum ) },
-            );
-
-  unless ( $cust_main ) {
-    $cust_main = new FS::cust_main { %args };
-    my $error = $cust_main->insert;
-    return $error if $error;
-  }
-
-  my @pkgs = grep { $_->part_pkg->freq } $cust_main->ncancelled_pkgs;
-  if ( scalar(@pkgs) > 1 ) {
-    return "Invalid account, should not be more then one active package ". #sic
-           "but found: ". scalar(@pkgs). " packages.";
-  }
-
-  my $part_pkg = qsearchs( 'part_pkg', { 'pkg' => $args{pkg} } ) 
-    or return "Unknown pkgpart: $args{pkg}"
-    if $args{pkg};
-
-
-  my $create_package = $args{pkg};        
-  if ( scalar(@pkgs) && $create_package ) {        
-    my $pkg = pop(@pkgs);
-        
-    if ( $part_pkg->pkgpart != $pkg->pkgpart ) {
-      my @cust_bill_pkg = $pkg->cust_bill_pkg();
-      if ( 1 == scalar(@cust_bill_pkg) ) {
-        my $cbp= pop(@cust_bill_pkg);
-        my $cust_bill = $cbp->cust_bill;
-        $cust_bill->delete();  #really?  wouldn't a credit be better?
-      }
-      $pkg->cancel();
-    } else {
-      $create_package = '';
-      $pkg->setfield('adjourn', str2time($args{adjourn}));
-      my $error = $pkg->replace();
-      return $error if $error;
-    }
-  }
-
-  if ( $create_package ) {
-    my $cust_pkg = new FS::cust_pkg ( {
-        'pkgpart' => $part_pkg->pkgpart,
-        'adjourn' => str2time( $args{adjourn} ),
-    } );
-
-    my $svcpart = $part_pkg->svcpart('svc_acct');
-
-    my $svc_acct = new FS::svc_acct ( {
-        'svcpart'   => $svcpart,
-        'username'  => $args{username},
-        '_password' => $args{_password},
-    } );
-
-    my $error = $cust_main->order_pkg( cust_pkg => $cust_pkg,
-                                       svcs     => [ $svc_acct ],
-    );
-    return $error if $error;
-  }
-    
-  if ( $args{promo_pkg} ) {
-    my $part_pkg =
-    qsearchs( 'part_pkg', { 'promo_code' =>  $args{promo_pkg} } )
-      or return "unknown pkgpart: $args{promo_pkg}";
-            
-    my $svcpart = $part_pkg->svcpart('svc_external')
-      or return "unknown svcpart: svc_external";
-
-    my $cust_pkg = new FS::cust_pkg ( {
-      'svcpart' => $svcpart,
-      'pkgpart' => $part_pkg->pkgpart,
-    } );
-
-    my $svc_ext = new FS::svc_external ( { 'svcpart'   => $svcpart } );
-    
-    my $ticket_subject = 'Send setup disk to customer '. $cust_main->custnum;
-    my $error = $cust_main->order_pkg ( cust_pkg       => $cust_pkg,
-                                        svcs           => [ $svc_ext ],
-                                        noexport       => 1,
-                                        ticket_subject => $ticket_subject,
-                                        ticket_queue   => "disk-$args{agentnum}",
-    );
-    return $error if $error;
-  }
-
-  my $error = $cust_main->bill();
-  return $error if $error;
-}
-
-sub process_recon {
-  my ( $agentnum, $id ) = @_;
-  my @recs = split /;/, $id;
-  my $err = '';
-  foreach my $rec ( @recs ) {
-    my @record = split /,/, $rec;
-    my $result = process_recon_record(@record, $agentnum);
-    $err .= "$result\n" if $result;
-  }
-  return $err;
-}
-
-sub process_recon_record {
-  my ( $agent_custid, $username, $_password, $daytime, $first, $last, $address1, $address2, $city, $state, $zip, $pkg, $adjourn, $agentnum) = @_;
-
-  warn "process_recon_record called with '". join("','", @_). "'\n" if $DEBUG;
-
-  my ($cust_pkg, $package);
-
-  my $cust_main =
-    qsearchs( 'cust_main',
-              { 'agent_custid' => $agent_custid, 'agentnum' => $agentnum },
-            );
-
-  my $comments = '';
-  if ( $cust_main ) {
-    my @cust_pkg = grep { $_->part_pkg->freq } $cust_main->ncancelled_pkgs;
-    if ( scalar(@cust_pkg) == 1) {
-      $cust_pkg = pop(@cust_pkg);
-      $package = $cust_pkg->part_pkg->pkg;
-      $comments = "$agent_custid wrong package, expected: $pkg found: $package"
-        if ( $pkg ne $package );
-    } else {
-      $comments = "invalid account, should be one active package but found: ".
-                 scalar(@cust_pkg). " packages.";
-    }
-  } else {
-    $comments =
-      "Customer not found agent_custid=$agent_custid, agentnum=$agentnum";
-  }
-
-  my $cust_recon = new FS::cust_recon( {
-    'recondate'     => time,
-    'agentnum'      => $agentnum,
-    'first'         => $first,
-    'last'          => $last,
-    'address1'      => $address1,
-    'address2'      => $address2,
-    'city'          => $city,
-    'state'         => $state,
-    'zip'           => $zip,
-    'custnum'       => $cust_main ? $cust_main->custnum : '', #really?
-    'status'        => $cust_main ? $cust_main->status : '',
-    'pkg'           => $package,
-    'adjourn'       => $cust_pkg ? $cust_pkg->adjourn : '',
-    'agent_custid'  => $agent_custid, # redundant?
-    'agent_pkg'     => $pkg,
-    'agent_adjourn' => str2time($adjourn),
-    'comments'      => $comments,
-  } );
-
-  warn Dumper($cust_recon) if $DEBUG;
-  my $error = $cust_recon->insert;
-  return $error if $error;
-
-  warn "process_recon_record returning $comments\n" if $DEBUG;
-
-  $comments;
-
-}
-
-sub check_username {
-  my $p = shift;
-
-  my $session = _cache->get($p->{'session_id'})
-    or return { 'error' => "Can't resume session" }; #better error message
-
-  my $svc_domain = qsearchs( 'svc_domain', { 'domain' => $p->{domain} } )
-    or return { 'error' => 'Unknown domain '. $p->{domain} };
-
-  my $svc_acct = qsearchs( 'svc_acct', { 'username' => $p->{user},
-                                         'domsvc'   => $svc_domain->svcnum,
-                                       },
-                         );
-
-  return { 'error' => $p->{user}. '@'. $p->{domain}. " alerady in use" } # sic
-    if $svc_acct;
-
-  return { 'error'   => '',
-           'message' => $p->{user}. '@'. $p->{domain}. " is free"
-  };
-}
-
-1;
index b02852b..01e0ebc 100644 (file)
@@ -45,12 +45,12 @@ use FS::payby;
 use FS::acct_rt_transaction;
 use FS::msg_template;
 
-$DEBUG = 0;
+$DEBUG = 1;
 $me = '[FS::ClientAPI::MyAccount]';
 
 use vars qw( @cust_main_editable_fields @location_editable_fields );
 @cust_main_editable_fields = qw(
-  first last daytime night fax mobile
+  first last company daytime night fax mobile
   locale
   payby payinfo payname paystart_month paystart_year payissue payip
   ss paytype paystate stateid stateid_state
@@ -121,6 +121,7 @@ sub skin_info {
             font title_color title_align title_size menu_bgcolor menu_fontsize
           )
       ),
+      'menu_disable' => [ $conf->config('selfservice-menu_disable',$agentnum) ],
       ( map { $_ => $conf->exists("selfservice-$_", $agentnum ) }
         qw( menu_skipblanks menu_skipheadings menu_nounderline no_logo )
       ),
@@ -635,11 +636,12 @@ sub billing_history {
 
       push @history, {
         'type'        => 'Line item',
-        'description' => $_->desc. ( $_->sdate && $_->edate
-                                       ? ' '. time2str('%d-%b-%Y', $_->sdate).
-                                         ' To '. time2str('%d-%b-%Y', $_->edate)
-                                       : ''
-                                   ),
+        'description' => $_->desc( $cust_main->locale ).
+                           ( $_->sdate && $_->edate
+                               ? ' '. time2str('%d-%b-%Y', $_->sdate).
+                                 ' To '. time2str('%d-%b-%Y', $_->edate)
+                               : ''
+                           ),
         'amount'      => sprintf('%.2f', $_->setup + $_->recur ),
         'date'        => $cust_bill->_date,
         'date_pretty' =>  time2str('%m/%d/%Y', $cust_bill->_date ),
@@ -1583,10 +1585,13 @@ sub list_pkgs {
                           my $primary_cust_svc = $_->primary_cust_svc;
                           +{ $_->hash,
                             $_->part_pkg->hash,
-                            pkg_label => $_->pkg_label,
+                            pkg_label => $_->pkg_locale,
                             status => $_->status,
                             part_svc =>
-                              [ map $_->hashref, $_->available_part_svc ],
+                              [ map { $_->hashref }
+                                  grep { $_->selfservice_access ne 'hidden' }
+                                    $_->available_part_svc
+                              ],
                             cust_svc => 
                               [ map { my $ref = { $_->hash,
                                                   label => [ $_->label ],
@@ -1600,7 +1605,9 @@ sub list_pkgs {
                                       $ref->{svchash}->{svcpart} =  $_->part_svc->svcpart
                                         if $_->part_svc->svcdb eq 'svc_phone'; # hack
                                       $ref;
-                                    } $_->cust_svc
+                                    }
+                                  grep { $_->part_svc->selfservice_access ne 'hidden' }
+                                    $_->cust_svc
                               ],
                             primary_cust_svc =>
                               $primary_cust_svc
@@ -1637,15 +1644,26 @@ sub list_svcs {
   }
 
   my @cust_svc = ();
+  my @cust_pkg_usage = ();
   #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
   foreach my $cust_pkg ( $p->{'ncancelled'} 
                          ? $cust_main->ncancelled_pkgs
                          : $cust_main->unsuspended_pkgs ) {
     next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
+    push @cust_pkg_usage, $cust_pkg->cust_pkg_usage;
   }
 
   @cust_svc = grep { $_->part_svc->selfservice_access ne 'hidden' } @cust_svc;
+  my %usage_pools;
+  foreach (@cust_pkg_usage) {
+    my $part = $_->part_pkg_usage;
+    my $tag = $part->description . ($part->shared ? 1 : 0);
+    my $row = $usage_pools{$tag} 
+          ||= [ $part->description, 0, 0, $part->shared ? 1 : 0 ];
+    $row->[1] += $_->minutes; # minutes remaining
+    $row->[2] += $part->minutes; # minutes total
+  }
 
   if ( $p->{'svcdb'} ) {
     my $svcdb = ref($p->{'svcdb'}) eq 'HASH'
@@ -1681,7 +1699,7 @@ sub list_svcs {
               'svcdb'          => $svcdb,
               'label'          => $label,
               'value'          => $value,
-              'pkg_label'      => $cust_pkg->pkg_label,
+              'pkg_label'      => $cust_pkg->pkg_locale,
               'pkg_status'     => $cust_pkg->status,
               'readonly'       => ($part_svc->selfservice_access eq 'readonly'),
             );
@@ -1717,7 +1735,34 @@ sub list_svcs {
               } else {
                 $hash{'name'} = $cust_main->name;
               }
+            } elsif ( $svcdb eq 'svc_phone' ) {
+              # could potentially show lots of things...
+              $hash{'outbound'} = 1;
+              $hash{'inbound'}  = 0;
+              if ( $part_pkg->plan eq 'voip_inbound' ) {
+                $hash{'outbound'} = 0;
+                $hash{'inbound'}  = 1;
+              } elsif ( $part_pkg->option('selfservice_inbound_format')
+                    or  $conf->config('selfservice-default_inbound_cdr_format')
+              ) {
+                $hash{'inbound'}  = 1;
+              }
+              foreach (qw(inbound outbound)) {
+                # hmm...we can't filter by status here, because there might
+                # not be cdr_terminations at all.  have to go by date.
+                # find all since the last bill date.
+                # XXX cdr types?  we are going to need them.
+                if ( $hash{$_} ) {
+                  my $sum_cdr = $svc_x->sum_cdrs(
+                    'inbound' => ( $_ eq 'inbound' ? 1 : 0 ),
+                    'begin'   => ($cust_pkg->last_bill || 0),
+                    'nonzero' => 1,
+                  );
+                  $hash{$_} = $sum_cdr->hashref;
+                }
+              }
             }
+
             # elsif ( $svcdb eq 'svc_phone' || $svcdb eq 'svc_port' ) {
             #  %hash = (
             #    %hash,
@@ -1728,6 +1773,11 @@ sub list_svcs {
           }
           @cust_svc
     ],
+    'usage_pools' => [
+      map { $usage_pools{$_} }
+      sort { $a cmp $b }
+      keys %usage_pools
+    ],
   };
 
 }
@@ -1782,8 +1832,14 @@ sub svc_status_hash {
 
 }
 
-sub set_svc_status_hash {
-  my $p = shift;
+sub set_svc_status_hash    { _svc_method_X(shift, 'export_setstatus') }
+sub set_svc_status_listadd { _svc_method_X(shift, 'export_setstatus_listadd') }
+sub set_svc_status_listdel { _svc_method_X(shift, 'export_setstatus_listdel') }
+sub set_svc_status_vacationadd { _svc_method_X(shift, 'export_setstatus_vacationadd') }
+sub set_svc_status_vacationdel { _svc_method_X(shift, 'export_setstatus_vacationdel') }
+
+sub _svc_method_X {
+  my( $p, $method ) = @_;
 
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
   return { 'error' => $session } if $context eq 'error';
@@ -1792,16 +1848,15 @@ sub set_svc_status_hash {
   my $svc_x = _customer_svc_x( $custnum, $p->{'svcnum'}, 'svc_acct')
     or return { 'error' => "Service not found" };
 
-  warn "set_svc_status_hash ". join(' / ', map "$_=>".$p->{$_}, keys %$p )
+  warn "$method ". join(' / ', map "$_=>".$p->{$_}, keys %$p )
     if $DEBUG;
-  my $error = $svc_x->export_setstatus($p); #$p? returns error?
+  my $error = $svc_x->$method($p); #$p? returns error?
   return { 'error' => $error } if $error;
 
   return {}; #? { 'error' => '' }
 
 }
 
-
 sub acct_forward_info {
   my $p = shift;
 
@@ -1985,7 +2040,7 @@ sub _list_cdr_usage {
   # we have to return the results all at once...
   my($svc_phone, $begin, $end, %opt) = @_;
   map [ $_->downstream_csv(%opt, 'keeparray' => 1) ],
-    $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, );
+    $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, %opt );
 }
 
 sub list_cdr_usage {
@@ -2015,18 +2070,21 @@ sub _usage_details {
   my %callback_opt;
   my $header = [];
   if ( $svcdb eq 'svc_phone' ) {
-    my $format   = $cust_pkg->part_pkg->option('output_format') || '';
-    $format = '' if $format =~ /^sum_/;
-    # sensible default if there is no format or it's a summary format
-    if ( $cust_pkg->part_pkg->plan eq 'voip_inbound' ) {
-      $format ||= 'source_default';
+    my $conf = FS::Conf->new;
+    my $format = '';
+    if ( $p->{inbound} ) {
+      $format = $cust_pkg->part_pkg->option('selfservice_inbound_format') 
+                || $conf->config('selfservice-default_inbound_cdr_format')
+                || 'source_default';
       $callback_opt{inbound} = 1;
+    } else {
+      $format = $cust_pkg->part_pkg->option('selfservice_format')
+                || $conf->config('selfservice-default_cdr_format')
+                || 'default';
     }
-    else {
-      $format ||= 'default';
-    }
-    
+
     $callback_opt{format} = $format;
+    $callback_opt{use_clid} = 1;
     $header = [ split(',', FS::cdr::invoice_header($format) ) ];
   }
 
@@ -2085,6 +2143,7 @@ sub _usage_details {
     'svcnum'    => $p->{svcnum},
     'beginning' => $p->{beginning},
     'ending'    => $p->{ending},
+    'inbound'   => $p->{inbound},
     'previous'  => ($previous > $start) ? $previous : $start,
     'next'      => ($next < $end) ? $next : $end,
     'header'    => $header,
index b7dcdbb..1dbb20b 100644 (file)
@@ -524,20 +524,13 @@ sub new_customer {
 
     my $template_cust = qsearchs('cust_main', { 'custnum' => $template_custnum } );
     return { 'error' => 'Configuration error' } unless $template_cust;
-    #XXX Copy template customer's locations
     $cust_main = new FS::cust_main ( {
       'agentnum'      => $agentnum,
       'refnum'        => $packet->{refnum}
                          || $conf->config('signup_server-default_refnum'),
 
       ( map { $_ => $template_cust->$_ } qw( 
-              last first company address1 address2 
-              city county state zip country
-              daytime night fax 
-
-              ship_last ship_first ship_company ship_address1 ship_address2
-              ship_city ship_county ship_state ship_zip ship_country
-              ship_daytime ship_night ship_fax
+              last first company daytime night fax 
             )
       ),
 
@@ -555,6 +548,9 @@ sub new_customer {
 
     } );
 
+    $bill_hash = { $template_cust->bill_location->location_hash };
+    $ship_hash = { $template_cust->ship_location->location_hash };
+
   } else {
 
     $cust_main = new FS::cust_main ( {
@@ -777,13 +773,15 @@ sub new_customer {
     #     " new customer: $bill_error"
     #  if $bill_error;
 
-    $bill_error = $cust_main->realtime_collect(
-       method        => FS::payby->payby2bop( $packet->{payby} ),
-       depend_jobnum => $placeholder->jobnum,
-       selfservice   => 1,
-    );
-    #warn "$me error collecting from new customer: $bill_error"
-    #  if $bill_error;
+    unless ( $packet->{payby} eq 'PREPAY' ) {
+      $bill_error = $cust_main->realtime_collect(
+         method        => FS::payby->payby2bop( $packet->{payby} ),
+         depend_jobnum => $placeholder->jobnum,
+         selfservice   => 1,
+      );
+      #warn "$me error collecting from new customer: $bill_error"
+      #  if $bill_error;
+    }
 
     if ($bill_error && ref($bill_error) eq 'HASH') {
       return { 'error' => '_collect',
index 7dd20c6..d720db2 100644 (file)
@@ -129,6 +129,10 @@ sub ss2clientapi {
   'svc_status_html'           => 'MyAccount/svc_status_html',
   'svc_status_hash'           => 'MyAccount/svc_status_hash',
   'set_svc_status_hash'       => 'MyAccount/set_svc_status_hash',
+  'set_svc_status_listadd'    => 'MyAccount/set_svc_status_listadd',
+  'set_svc_status_listdel'    => 'MyAccount/set_svc_status_listdel',
+  'set_svc_status_vacationadd'=> 'MyAccount/set_svc_status_vacationadd',
+  'set_svc_status_vacationdel'=> 'MyAccount/set_svc_status_vacationdel',
   'acct_forward_info'         => 'MyAccount/acct_forward_info',
   'process_acct_forward'      => 'MyAccount/process_acct_forward',
   'list_dsl_devices'          => 'MyAccount/list_dsl_devices',   
index d11916f..6a19ff4 100644 (file)
@@ -717,6 +717,18 @@ my %batch_gateway_options = (
   },
 );
 
+my @cdr_formats = (
+  '' => '',
+  'default' => 'Default',
+  'source_default' => 'Default with source',
+  'accountcode_default' => 'Default plus accountcode',
+  'description_default' => 'Default with description field as destination',
+  'basic' => 'Basic',
+  'simple' => 'Simple',
+  'simple2' => 'Simple with source',
+  'accountcode_simple' => 'Simple with accountcode',
+);
+
 # takes the reason class (C, R, S) as an argument
 sub reason_type_options {
   my $reason_class = shift;
@@ -985,6 +997,14 @@ sub reason_type_options {
   },
 
   {
+    'key'         => 'currency',
+    'section'     => 'billing',
+    'description' => 'Currency',
+    'type'        => 'select',
+    'select_enum' => [ '', qw( USD AUD CAD DKK EUR GBP ILS JPY NZD XAF ) ],
+  },
+
+  {
     'key'         => 'business-batchpayment-test_transaction',
     'section'     => 'billing',
     'description' => 'Turns on the Business::BatchPayment test_mode flag.  Note that not all gateway modules support this flag; if yours does not, using the batch gateway will fail.',
@@ -1509,8 +1529,18 @@ and customer address. Include units.',
     'section'     => 'invoicing',
     'description' => 'Split invoice into sections and label according to package category when enabled.',
     'type'        => 'checkbox',
+    'per_agent'   => 1,
   },
 
+  #quotations seem broken-ish with sections ATM?
+  #{ 
+  #  'key'         => 'quotation_sections',
+  #  'section'     => 'invoicing',
+  #  'description' => 'Split quotations into sections and label according to package category when enabled.',
+  #  'type'        => 'checkbox',
+  #  'per_agent'   => 1,
+  #},
+
   { 
     'key'         => 'usage_class_as_a_section',
     'section'     => 'invoicing',
@@ -1608,6 +1638,7 @@ and customer address. Include units.',
     'section'     => 'required',
     'description' => 'Print command for paper invoices, for example `lpr -h\'',
     'type'        => 'text',
+    'per_agent'   => 1,
   },
 
   {
@@ -2053,7 +2084,7 @@ and customer address. Include units.',
     'key'         => 'locale',
     'section'     => 'UI',
     'description' => 'Default locale',
-    'type'        => 'select',
+    'type'        => 'select-sub',
     'options_sub' => sub {
       map { $_ => FS::Locales->description($_) } FS::Locales->locales;
     },
@@ -3525,7 +3556,7 @@ and customer address. Include units.',
     'section'     => 'billing',
     'description' => 'Default format for batches.',
     'type'        => 'select',
-    'select_enum' => [ 'csv-td_canada_trust-merchant_pc_batch',
+    'select_enum' => [ 'NACHA', 'csv-td_canada_trust-merchant_pc_batch',
                        'csv-chase_canada-E-xactBatch', 'BoM', 'PAP',
                        'paymentech', 'ach-spiritone', 'RBC'
                     ]
@@ -3587,9 +3618,9 @@ and customer address. Include units.',
     'section'     => 'billing',
     'description' => 'Fixed (unchangeable) format for electronic check batches.',
     'type'        => 'select',
-    'select_enum' => [ 'csv-td_canada_trust-merchant_pc_batch', 'BoM', 'PAP',
-                       'paymentech', 'ach-spiritone', 'RBC', 'td_eft1464',
-                       'eft_canada'
+    'select_enum' => [ 'NACHA', 'csv-td_canada_trust-merchant_pc_batch', 'BoM',
+                       'PAP', 'paymentech', 'ach-spiritone', 'RBC',
+                       'td_eft1464', 'eft_canada'
                      ]
   },
 
@@ -3643,13 +3674,6 @@ and customer address. Include units.',
   },
 
   {
-    'key'         => 'batch-manual_approval',
-    'section'     => 'billing',
-    'description' => 'Allow manual batch closure, which will approve all payments that do not yet have a status.  This is not advised, but is needed for payment processors that provide a report of rejected rather than approved payments.',
-    'type'        => 'checkbox',
-  },
-
-  {
     'key'         => 'batchconfig-eft_canada',
     'section'     => 'billing',
     'description' => 'Configuration for EFT Canada batching, four lines: 1. SFTP username, 2. SFTP password, 3. Transaction code, 4. Number of days to delay process date.',
@@ -3658,6 +3682,34 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'batchconfig-nacha-destination',
+    'section'     => 'billing',
+    'description' => 'Configuration for NACHA batching, Destination (9 digit transit routing number).',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'batchconfig-nacha-destination_name',
+    'section'     => 'billing',
+    'description' => 'Configuration for NACHA batching, Destination (Bank Name, up to 23 characters).',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'batchconfig-nacha-origin',
+    'section'     => 'billing',
+    'description' => 'Configuration for NACHA batching, Origin (your 10-digit company number, IRS tax ID recommended).',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'batch-manual_approval',
+    'section'     => 'billing',
+    'description' => 'Allow manual batch closure, which will approve all payments that do not yet have a status.  This is not advised unless needed for specific payment processors that provide a report of rejected rather than approved payments.',
+    'type'        => 'checkbox',
+  },
+
+  {
     'key'         => 'batch-spoolagent',
     'section'     => 'billing',
     'description' => 'Store payment batches per-agent.',
@@ -3722,20 +3774,6 @@ and customer address. Include units.',
   },
 
   {
-    'key'         => 'cust_main-skeleton_tables',
-    'section'     => '',
-    'description' => 'Tables which will have skeleton records inserted into them for each customer.  Syntax for specifying tables is unfortunately a tricky perl data structure for now.',
-    'type'        => 'textarea',
-  },
-
-  {
-    'key'         => 'cust_main-skeleton_custnum',
-    'section'     => '',
-    'description' => 'Customer number specifying the source data to copy into skeleton tables for new customers.',
-    'type'        => 'text',
-  },
-
-  {
     'key'         => 'cust_main-enable_birthdate',
     'section'     => 'UI',
     'description' => 'Enable tracking of a birth date with each customer record',
@@ -3785,6 +3823,13 @@ and customer address. Include units.',
     'type'        => 'checkbox',
   },
 
+  {
+    'key'         => 'fuzzy-fuzziness',
+    'section'     => 'UI',
+    'description' => 'Set the "fuzziness" of fuzzy searching (see the String::Approx manpage for details).  Defaults to 10%',
+    'type'        => 'text',
+  },
+
   { 'key'         => 'pkg_referral',
     'section'     => '',
     'description' => 'Enable package-specific advertising sources.',
@@ -3916,6 +3961,19 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'cust_bill-line_item-date_style-non_monthly',
+    'section'     => 'billing',
+    'description' => 'If set, override cust_bill-line_item-date_style for non-monthly charges.',
+    'type'        => 'select',
+    'select_hash' => [ ''           => 'Default',
+                       'start_end'  => 'STARTDATE-ENDDATE',
+                       'month_of'   => 'Month of MONTHNAME',
+                       'X_month'    => 'DATE_DESC MONTHNAME',
+                     ],
+    'per_agent'   => 1,
+  },
+
+  {
     'key'         => 'cust_bill-line_item-date_description',
     'section'     => 'billing',
     'description' => 'Text to display for "DATE_DESC" when using cust_bill-line_item-date_style DATE_DESC MONTHNAME.',
@@ -3954,7 +4012,7 @@ and customer address. Include units.',
     'type'        => 'select',
     'multiple'    => 1,
     'select_hash' => [ 
-      'address1' => 'Billing address',
+      #'address1' => 'Billing address',
     ],
   },
 
@@ -4128,7 +4186,7 @@ and customer address. Include units.',
   {
     'key'         => 'census_year',
     'section'     => 'UI',
-    'description' => 'The year to use in census tract lookups',
+    'description' => 'The year to use in census tract lookups.  NOTE: you need to select 2012 for Year 2010 Census tract codes.  A selection of 2011 or 2010 provides Year 2000 Census tract codes.  Use the freeside-censustract-update tool if exisitng customers need to be changed.',
     'type'        => 'select',
     'select_enum' => [ qw( 2012 2011 2010 ) ],
   },
@@ -4462,6 +4520,31 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'selfservice-menu_disable',
+    'section'     => 'self-service',
+    'description' => 'Disable the selected menu entries in the self-service menu',
+    'type'        => 'selectmultiple',
+    'select_enum' => [ #false laziness w/myaccount_menu.html
+                       'Overview',
+                       'Purchase',
+                       'Purchase additional package',
+                       'Recharge my account with a credit card',
+                       'Recharge my account with a check',
+                       'Recharge my account with a prepaid card',
+                       'View my usage',
+                       'Create a ticket',
+                       'Setup my services',
+                       'Change my information',
+                       'Change billing address',
+                       'Change service address',
+                       'Change payment information',
+                       'Change password(s)',
+                       'Logout',
+                     ],
+    'per_agent'   => 1,
+  },
+
+  {
     'key'         => 'selfservice-menu_skipblanks',
     'section'     => 'self-service',
     'description' => 'Skip blank (spacer) entries in the self-service menu',
@@ -4547,23 +4630,6 @@ and customer address. Include units.',
   },
 
   {
-    'key'         => 'selfservice-bulk_format',
-    'section'     => 'deprecated',
-    'description' => 'Parameter arrangement for selfservice bulk features',
-    'type'        => 'select',
-    'select_enum' => [ '', 'izoom-soap', 'izoom-ftp' ],
-    'per_agent'   => 1,
-  },
-
-  {
-    'key'         => 'selfservice-bulk_ftp_dir',
-    'section'     => 'deprecated',
-    'description' => 'Enable bulk ftp provisioning in this folder',
-    'type'        => 'text',
-    'per_agent'   => 1,
-  },
-
-  {
     'key'         => 'signup-no_company',
     'section'     => 'self-service',
     'description' => "Don't display a field for company name on signup.",
@@ -4706,6 +4772,13 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'cdr-taqua-callerid_rewrite',
+    'section'     => 'telephony',
+    'description' => 'For the Taqua CDR format, pull Caller ID blocking information from secondary CDRs.',
+    'type'        => 'checkbox',
+  },
+
+  {
     'key'         => 'cdr-asterisk_australia_rewrite',
     'section'     => 'telephony',
     'description' => 'For Asterisk CDRs, assign CDR type numbers based on Australian conventions.',
@@ -4713,6 +4786,13 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'cdr-gsm_tap3-sender',
+    'section'     => 'telephony',
+    'description' => 'GSM TAP3 Sender network (5 letter code)',
+    'type'        => 'text',
+  },
+
+  {
     'key'         => 'cust_pkg-show_autosuspend',
     'section'     => 'UI',
     'description' => 'Show package auto-suspend dates.  Use with caution for now; can slow down customer view for large insallations.',
@@ -4772,7 +4852,7 @@ and customer address. Include units.',
   {
     'key'         => 'svc_broadband-manage_link',
     'section'     => 'UI',
-    'description' => 'URL for svc_broadband "Manage Device" link.  The following substitutions are available: $ip_addr.',
+    'description' => 'URL for svc_broadband "Manage Device" link.  The following substitutions are available: $ip_addr and $mac_addr.',
     'type'        => 'text',
   },
 
@@ -4871,7 +4951,7 @@ and customer address. Include units.',
   {
     'key'         => 'pkg-balances',
     'section'     => 'billing',
-    'description' => 'Enable experimental package balances.  Not recommended for general use.',
+    'description' => 'Enable per-package balances.',
     'type'        => 'checkbox',
   },
 
@@ -5125,6 +5205,13 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'invoice_payment_details',
+    'section'     => 'invoicing',
+    'description' => 'When displaying payments on an invoice, show the payment method used, including the check or credit card number.  Credit card numbers will be masked.',
+    'type'        => 'checkbox',
+  },
+
+  {
     'key'         => 'cust_main-status_module',
     'section'     => 'UI',
     'description' => 'Which module to use for customer status display.  The "Classic" module (the default) considers accounts with cancelled recurring packages but un-cancelled one-time charges Inactive.  The "Recurring" module considers those customers Cancelled.  Similarly for customers with suspended recurring packages but one-time charges.', #other differences?
@@ -5146,6 +5233,13 @@ and customer address. Include units.',
     'type'        => 'checkbox',
   },
 
+  { 
+    'key'         => 'username-exclamation',
+    'section'     => 'username',
+    'description' => 'Allow the exclamation character (!) in usernames.',
+    'type'        => 'checkbox',
+  },
+
   {
     'key'         => 'ie-compatibility_mode',
     'section'     => 'UI',
@@ -5260,6 +5354,19 @@ and customer address. Include units.',
                            $cdr_type ? $cdr_type->cdrtypename : '';
                         },
   },
+
+  {
+    'key'         => 'cdr-minutes_priority',
+    'section'     => 'telephony',
+    'description' => 'Priority rule for assigning included minutes to CDRs.',
+    'type'        => 'select',
+    'select_hash' => [
+      ''          => 'No specific order',
+      'time'      => 'Chronological',
+      'rate_high' => 'Highest rate first',
+      'rate_low'  => 'Lowest rate first',
+    ],
+  },
   
   {
     'key'         => 'brand-agent',
@@ -5283,6 +5390,22 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'selfservice-default_cdr_format',
+    'section'     => 'self-service',
+    'description' => 'Format for showing outbound CDRs in self-service.  The per-package option overrides this.',
+    'type'        => 'select',
+    'select_hash' => \@cdr_formats,
+  },
+
+  {
+    'key'         => 'selfservice-default_inbound_cdr_format',
+    'section'     => 'self-service',
+    'description' => 'Format for showing inbound CDRs in self-service.  The per-package option overrides this.  Leave blank to avoid showing these CDRs.',
+    'type'        => 'select',
+    'select_hash' => \@cdr_formats,
+  },
+
+  {
     'key'         => 'logout-timeout',
     'section'     => 'UI',
     'description' => 'If set, automatically log users out of the backoffice after this many minutes.',
@@ -5307,6 +5430,13 @@ and customer address. Include units.',
     'type'        => 'text',
   },
 
+  {
+    'key'         => 'report-cust_pay-select_time',
+    'section'     => 'UI',
+    'description' => 'Enable time selection on payment and refund reports.',
+    'type'        => 'checkbox',
+  },
+
   { 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" },
index 6e110e8..98ce8fa 100644 (file)
@@ -201,7 +201,8 @@ sub bill_where {
   # generate where_pkg/where_event search clause
   ###
 
-  my $billtime = day_end($time);
+  my $conf = new FS::Conf;
+  my $billtime = $conf->exists('next-bill-ignore-time') ? day_end($time) : $time;
 
   # select * from cust_main where
   my $where_pkg = <<"END";
index 628c680..03ed366 100644 (file)
@@ -470,7 +470,7 @@ sub spool_upload {
 
 }
 
-=item send_report CONFIG PARAMS
+=item prepare_report CONFIG PARAMS
 
 Retrieves the config value named CONFIG, parses it as a Text::Template,
 extracts "to" and "subject" headers, and returns a hash that can be passed
index 6ad136b..ed936a5 100644 (file)
@@ -1,6 +1,8 @@
 package FS::L10N::en_us;
-use base qw(FS::L10N);
+use base qw(FS::L10N::DBI);
 
-our %Lexicon = ( _AUTO=>1 );
+#prevents english "translation" via FS::L10N::DBI, FS::Msgcat::_gettext already
+# does the same sort of fallback 
+#our %Lexicon = ( _AUTO=>1 );
 
 1;
index 2bc1596..1553a42 100644 (file)
@@ -77,7 +77,7 @@ if ( -e $addl_handler_use_file ) {
   use HTML::TableExtract qw(tree);
   use HTML::FormatText;
   use HTML::Defang;
-  use JSON;
+  use JSON::XS;
 #  use XMLRPC::Transport::HTTP;
 #  use XMLRPC::Lite; # for XMLRPC::Serializer
   use MIME::Base64;
@@ -160,6 +160,7 @@ if ( -e $addl_handler_use_file ) {
   use FS::cust_credit;
   use FS::cust_credit_bill;
   use FS::cust_main;
+  use FS::h_cust_main;
   use FS::cust_main::Search qw(smart_search);
   use FS::cust_main::Import;
   use FS::cust_main_county;
@@ -332,6 +333,12 @@ if ( -e $addl_handler_use_file ) {
   use FS::GeocodeCache;
   use FS::log;
   use FS::log_context;
+  use FS::part_pkg_usage_class;
+  use FS::cust_pkg_usage;
+  use FS::part_pkg_usage_class;
+  use FS::part_pkg_usage;
+  use FS::cdr_cust_pkg_usage;
+  use FS::part_pkg_msgcat;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index 096ec8a..de9fb52 100644 (file)
@@ -699,7 +699,8 @@ sub generate_ps {
   open(POSTSCRIPT, "<$file.ps")
     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
 
-  unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
+  unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex")
+    unless $FS::CurrentUser::CurrentUser->option('save_tmp_typesetting');
 
   my $ps = '';
 
@@ -757,7 +758,8 @@ sub generate_pdf {
   open(PDF, "<$file.pdf")
     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
 
-  unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
+  unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex")
+    unless $FS::CurrentUser::CurrentUser->option('save_tmp_typesetting');
 
   my $pdf = '';
   while (<PDF>) {
@@ -800,16 +802,32 @@ sub _pslatex {
 
 }
 
-=item do_print ARRAYREF
+=item do_print ARRAYREF [, OPTION => VALUE ... ]
 
 Sends the lines in ARRAYREF to the printer.
 
+Options available are:
+
+=over 4
+
+=item agentnum
+
+Uses this agent's 'lpr' configuration setting override instead of the global
+value.
+
+=item lpr
+
+Uses this command instead of the configured lpr command (overrides both the
+global value and agentnum).
+
 =cut
 
 sub do_print {
-  my $data = shift;
+  my( $data, %opt ) = @_;
 
-  my $lpr = $conf->config('lpr');
+  my $lpr = ( exists($opt{'lpr'}) && $opt{'lpr'} )
+              ? $opt{'lpr'}
+              : $conf->config('lpr', $opt{'agentnum'} );
 
   my $outerr = '';
   run3 $lpr, $data, \$outerr, \$outerr;
index 9c12e64..2fff906 100644 (file)
@@ -2,8 +2,8 @@ package FS::Misc::DateTime;
 
 use base qw( Exporter );
 use vars qw( @EXPORT_OK );
-use POSIX;
 use Carp;
+use Time::Local;
 use Date::Parse;
 use DateTime::Format::Natural;
 use FS::Conf;
@@ -49,7 +49,7 @@ sub parse_datetime {
       #carp "WARNING: can't parse date: ". $parser->error;
       #return '';
       #huh, very common, we still need the "partially" (fully enough for our purposes) parsed date.
-      $dt->epoch;
+      return $dt->epoch;
     }
   } else {
     return str2time($string, $tz);
@@ -59,24 +59,17 @@ sub parse_datetime {
 
 =item day_end TIME
 
-If the next-bill-ignore-time configuration setting is turned off, just 
-returns the passed-in value.
-
-If the next-bill-ignore-time configuration setting is turned on, parses TIME
-as an integer UNIX timestamp and returns a new timestamp with the same date but
-23:59:59 for the time.
+Parses TIME as an integer UNIX timestamp and returns a new timestamp with the
+same date but 23:59:59 for the time.
 
 =cut
 
 sub day_end {
     my $time = shift;
 
-    my $conf = new FS::Conf;
-    return $time unless $conf->exists('next-bill-ignore-time');
-
     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
         localtime($time);
-    mktime(59,59,23,$mday,$mon,$year,$wday,$yday,$isdst);
+    timelocal(59,59,23,$mday,$mon,$year);
 }
 
 =back
index 5cb10b2..2ad8311 100644 (file)
@@ -81,7 +81,7 @@ sub get_censustract_ffiec {
 
       my($zip5, $zip4) = split('-',$location->{zip});
 
-      $year ||= '2011'; #2012 per http://transition.fcc.gov/form477/techfaqs.html soon/now?
+      $year ||= '2012';
       my @ffiec_args = (
         __VIEWSTATE => $viewstate,
         __EVENTVALIDATION => $eventvalidation,
index ca68c35..bdf3bcf 100644 (file)
@@ -458,7 +458,13 @@ sub qsearch {
 #    grep defined( $record->{$_} ) && $record->{$_} ne '', @fields
 #  ) or croak "Error executing \"$statement\": ". $sth->errstr;
 
-  $sth->execute or croak "Error executing \"$statement\": ". $sth->errstr;
+  my $ok = $sth->execute;
+  if (!$ok) {
+    my $error = "Error executing \"$statement\"";
+    $error .= ' (' . join(', ', map {"'$_'"} @value) . ')' if @value;
+    $error .= ': '. $sth->errstr;
+    croak $error;
+  }
 
   my $table = $stable[0];
   my $pkey = '';
@@ -1451,6 +1457,7 @@ sub process_batch_import {
     format_sep_chars           => $opt->{format_sep_chars},
     format_fixedlength_formats => $opt->{format_fixedlength_formats},
     format_xml_formats         => $opt->{format_xml_formats},
+    format_asn_formats         => $opt->{format_asn_formats},
     format_row_callbacks       => $opt->{format_row_callbacks},
     #per-import
     job                        => $job,
@@ -1533,8 +1540,9 @@ sub batch_import {
   my $file    = $param->{file};
   my $params  = $param->{params} || {};
 
-  my( $type, $header, $sep_char, $fixedlength_format, 
-      $xml_format, $row_callback, @fields );
+  my( $type, $header, $sep_char,
+      $fixedlength_format, $xml_format, $asn_format,
+      $row_callback, @fields );
 
   my $postinsert_callback = '';
   $postinsert_callback = $param->{'postinsert_callback'}
@@ -1572,6 +1580,11 @@ sub batch_import {
         ? $param->{'format_xml_formats'}{ $param->{'format'} }
         : '';
 
+    $asn_format =
+      $param->{'format_asn_formats'}
+        ? $param->{'format_asn_formats'}{ $param->{'format'} }
+        : '';
+
     $row_callback =
       $param->{'format_row_callbacks'}
         ? $param->{'format_row_callbacks'}{ $param->{'format'} }
@@ -1611,11 +1624,12 @@ sub batch_import {
   my $count;
   my $parser;
   my @buffer = ();
+  my $asn_header_buffer;
   if ( $type eq 'csv' || $type eq 'fixedlength' ) {
 
     if ( $type eq 'csv' ) {
 
-      my %attr = ();
+      my %attr = ( 'binary' => 1, );
       $attr{sep_char} = $sep_char if $sep_char;
       $parser = new Text::CSV_XS \%attr;
 
@@ -1652,7 +1666,9 @@ sub batch_import {
     $count++;
 
     $row = $header || 0;
+
   } elsif ( $type eq 'xml' ) {
+
     # FS::pay_batch
     eval "use XML::Simple;";
     die $@ if $@;
@@ -1668,6 +1684,26 @@ sub batch_import {
     $rows = $rows->{$_} foreach @$xmlrow;
     $rows = [ $rows ] if ref($rows) ne 'ARRAY';
     $count = @buffer = @$rows;
+
+  } elsif ( $type eq 'asn.1' ) {
+
+    eval "use Convert::ASN1";
+    die $@ if $@;
+
+    my $asn = Convert::ASN1->new;
+    $asn->prepare( $asn_format->{'spec'} ) or die $asn->error;
+
+    $parser = $asn->find( $asn_format->{'macro'} ) or die $asn->error;
+
+    my $data = slurp($file);
+    my $asn_output = $parser->decode( $data )
+      or die "No ". $asn_format->{'macro'}. " found\n";
+
+    $asn_header_buffer = &{ $asn_format->{'header_buffer'} }( $asn_output );
+
+    my $rows = &{ $asn_format->{'arrayref'} }( $asn_output );
+    $count = @buffer = @$rows;
+
   } else {
     die "Unknown file type $type\n";
   }
@@ -1711,6 +1747,7 @@ sub batch_import {
   while (1) {
 
     my @columns = ();
+    my %hash = %$params;
     if ( $type eq 'csv' ) {
 
       last unless scalar(@buffer);
@@ -1747,16 +1784,27 @@ sub batch_import {
       #warn $z++. ": $_\n" for @columns;
 
     } elsif ( $type eq 'xml' ) {
+
       # $parser = [ 'Column0Key', 'Column1Key' ... ]
       last unless scalar(@buffer);
       my $row = shift @buffer;
       @columns = @{ $row }{ @$parser };
+
+    } elsif ( $type eq 'asn.1' ) {
+
+      last unless scalar(@buffer);
+      my $row = shift @buffer;
+      &{ $asn_format->{row_callback} }( $row, $asn_header_buffer )
+        if $asn_format->{row_callback};
+      foreach my $key ( keys %{ $asn_format->{map} } ) {
+        $hash{$key} = &{ $asn_format->{map}{$key} }( $row, $asn_header_buffer );
+      }
+
     } else {
       die "Unknown file type $type\n";
     }
 
     my @later = ();
-    my %hash = %$params;
 
     foreach my $field ( @fields ) {
 
@@ -2051,11 +2099,18 @@ is an error, returns the error, otherwise returns false.
 
 sub ut_money {
   my($self,$field)=@_;
-  $self->setfield($field, 0) if $self->getfield($field) eq '';
-  $self->getfield($field) =~ /^\s*(\-)?\s*(\d*)(\.\d{2})?\s*$/
-    or return "Illegal (money) $field: ". $self->getfield($field);
-  #$self->setfield($field, "$1$2$3" || 0);
-  $self->setfield($field, ( ($1||''). ($2||''). ($3||'') ) || 0);
+
+  if ( $self->getfield($field) eq '' ) {
+    $self->setfield($field, 0);
+  } elsif ( $self->getfield($field) =~ /^\s*(\-)?\s*(\d*)(\.\d{1})\s*$/ ) {
+    #handle one decimal place without barfing out
+    $self->setfield($field, ( ($1||''). ($2||''). ($3.'0') ) || 0);
+  } elsif ( $self->getfield($field) =~ /^\s*(\-)?\s*(\d*)(\.\d{2})?\s*$/ ) {
+    $self->setfield($field, ( ($1||''). ($2||''). ($3||'') ) || 0);
+  } else {
+    return "Illegal (money) $field: ". $self->getfield($field);
+  }
+
   '';
 }
 
@@ -2466,10 +2521,29 @@ sub ut_name {
 #  warn "ut_name allowed alphanumerics: +(sort grep /\w/, map { chr() } 0..255), "\n";
   $self->getfield($field) =~ /^([\w \,\.\-\']+)$/
     or return gettext('illegal_name'). " $field: ". $self->getfield($field);
-  $self->setfield($field,$1);
+  my $name = $1;
+  $name =~ s/^\s+//; 
+  $name =~ s/\s+$//; 
+  $name =~ s/\s+/ /g;
+  $self->setfield($field, $name);
   '';
 }
 
+=item ut_namen COLUMN
+
+Check/untaint proper names; allows alphanumerics, spaces and the following
+punctuation: , . - '
+
+May not be null.
+
+=cut
+
+sub ut_namen {
+  my( $self, $field ) = @_;
+  return $self->setfield($field, '') if $self->getfield($field) =~ /^$/;
+  $self->ut_name($field);
+}
+
 =item ut_zip COLUMN
 
 Check/untaint zip codes.
index 49bb8a8..fd08814 100644 (file)
@@ -22,26 +22,26 @@ Documentation.
 =cut
 
 @upload = qw(
- <200kpbs
- 200-768kpbs
+ <200kbps
+ 200-768kbps
  768kbps-1.5mbps
  1.5-3mpbs
  3-6mbps
  6-10mbps
  10-25mbps
  25-100mbps
- >100bmps
+ >100mbps
 );
 
 @download = qw(
- 200-768kpbs
+ 200-768kbps
  768kbps-1.5mbps
- 1.5-3mpbs
+ 1.5-3mbps
  3-6mbps
  6-10mbps
  10-25mbps
  25-100mbps
- >100bmps
+ >100mbps
 );
 
 @technology = (
index cbcd27b..eb73ccb 100644 (file)
@@ -772,7 +772,7 @@ sub tables_hashref {
         'format',  'char', 'NULL', 1, '', '',
         'classnum', 'int', 'NULL', '', '', '',
         'duration', 'int', 'NULL', '',  0, '',
-        'phonenum', 'varchar', 'NULL', 15, '', '',
+        'phonenum', 'varchar', 'NULL', 25, '', '',
         'accountcode', 'varchar',  'NULL',      20, '', '',
         'startdate',  @date_type, '', '', 
         'regionname', 'varchar', 'NULL', $char_d, '', '',
@@ -875,7 +875,7 @@ sub tables_hashref {
         'format',  'char', 'NULL', 1, '', '',
         'classnum', 'int', 'NULL', '', '', '',
         'duration', 'int', 'NULL', '',  0, '',
-        'phonenum', 'varchar', 'NULL', 15, '', '',
+        'phonenum', 'varchar', 'NULL', 25, '', '',
         'accountcode', 'varchar',  'NULL',      20, '', '',
         'startdate',  @date_type, '', '', 
         'regionname', 'varchar', 'NULL', $char_d, '', '',
@@ -1080,6 +1080,7 @@ sub tables_hashref {
         'locale', 'varchar', 'NULL', 16, '', '', 
         'calling_list_exempt', 'char', 'NULL', 1, '', '',
         'invoice_noemail', 'char', 'NULL', 1, '', '',
+        'message_noemail', 'char', 'NULL', 1, '', '',
         'bill_locationnum', 'int', 'NULL', '', '', '',
         'ship_locationnum', 'int', 'NULL', '', '', '',
       ],
@@ -1225,6 +1226,8 @@ sub tables_hashref {
     'quotation_pkg' => {
       'columns' => [
         'quotationpkgnum',   'serial',     '', '', '', '', 
+        'quotationnum',         'int', 'NULL', '', '', '', #shouldn't be null,
+                                                           # but history...
         'pkgpart',              'int',     '', '', '', '', 
         'locationnum',          'int', 'NULL', '', '', '',
         'start_date',      @date_type,             '', '', 
@@ -1688,13 +1691,14 @@ sub tables_hashref {
         'zip',      'varchar', 'NULL', 10, '', '', 
         'country',  'char', '',     2, '', '', 
         #        'trancode', 'int', '', '', '', ''
-        'payby',    'char',   '',     4, '', '', # CARD/BILL/COMP, should be
-        'payinfo',  'varchar', '',     512, '', '', 
+        'payby',    'char',        '',       4, '', '',
+        'payinfo',  'varchar', 'NULL',     512, '', '', 
         #'exp',      @date_type, '', ''
-        'exp',      'varchar', 'NULL',     11, '', '', 
+        'exp',      'varchar', 'NULL',      11, '', '', 
         'payname',  'varchar', 'NULL', $char_d, '', '', 
         'amount',   @money_type, '', '', 
-        'status',   'varchar', 'NULL',     $char_d, '', '', 
+        'status',   'varchar', 'NULL', $char_d, '', '', 
+        'error_message',   'varchar', 'NULL', $char_d, '', '',
       ],
       'primary_key' => 'paybatchnum',
       'unique' => [],
@@ -1717,6 +1721,7 @@ sub tables_hashref {
         'custnum',             'int',     '', '', '', '', 
         'pkgpart',             'int',     '', '', '', '', 
         'pkgbatch',        'varchar', 'NULL', $char_d, '', '',
+        'contactnum',          'int', 'NULL', '', '', '', 
         'locationnum',         'int', 'NULL', '', '', '',
         'otaker',          'varchar', 'NULL', 32, '', '', 
         'usernum',             'int', 'NULL', '', '', '',
@@ -1738,6 +1743,8 @@ sub tables_hashref {
         'change_pkgnum',       'int', 'NULL', '', '', '',
         'change_pkgpart',      'int', 'NULL', '', '', '',
         'change_locationnum',  'int', 'NULL', '', '', '',
+        'main_pkgnum',         'int', 'NULL', '', '', '',
+        'pkglinknum',          'int', 'NULL', '', '', '',
         'manual_flag',        'char', 'NULL',  1, '', '', 
         'no_auto',            'char', 'NULL',  1, '', '', 
         'quantity',            'int', 'NULL', '', '', '',
@@ -1812,6 +1819,30 @@ sub tables_hashref {
       'index'  => [ [ 'pkgnum' ], [ 'discountnum' ], [ 'usernum' ], ],
     },
 
+    'cust_pkg_usage' => {
+      'columns' => [
+        'pkgusagenum', 'serial', '', '', '', '',
+        'pkgnum',         'int', '', '', '', '',
+        'minutes',        'int', '', '', '', '',
+        'pkgusagepart',   'int', '', '', '', '',
+      ],
+      'primary_key' => 'pkgusagenum',
+      'unique' => [],
+      'index'  => [ [ 'pkgnum' ], [ 'pkgusagepart' ] ],
+    },
+
+    'cdr_cust_pkg_usage' => {
+      'columns' => [
+        'cdrusagenum', 'bigserial', '', '', '', '',
+        'acctid',      'bigint',    '', '', '', '',
+        'pkgusagenum', 'int',       '', '', '', '',
+        'minutes',     'int',       '', '', '', '',
+      ],
+      'primary_key' => 'cdrusagenum',
+      'unique' => [],
+      'index'  => [ [ 'pkgusagenum' ], [ 'acctid' ] ],
+    },
+
     'cust_bill_pkg_discount' => {
       'columns' => [
         'billpkgdiscountnum', 'serial',        '', '', '', '',
@@ -1981,6 +2012,19 @@ sub tables_hashref {
                  ],
     },
 
+    'part_pkg_msgcat' => {
+      'columns' => [
+        'pkgpartmsgnum',  'serial',     '',        '', '', '',
+        'pkgpart',           'int',     '',        '', '', '',
+        'locale',        'varchar',     '',        16, '', '',
+        'pkg',           'varchar',     '',   $char_d, '', '', #longer/no limit?
+        'comment',       'varchar', 'NULL', 2*$char_d, '', '', #longer/no limit?
+      ],
+      'primary_key' => 'pkgpartmsgnum',
+      'unique'      => [ [ 'pkgpart', 'locale' ] ],
+      'index'       => [],
+    },
+
     'part_pkg_link' => {
       'columns' => [
         'pkglinknum',  'serial',   '',      '', '', '',
@@ -2109,7 +2153,8 @@ sub tables_hashref {
         'preserve',              'char', 'NULL',         1, '', '',
         'selfservice_access', 'varchar', 'NULL',   $char_d, '', '',
         'classnum',               'int', 'NULL',        '', '', '',
-      ],
+        'restrict_edit_password','char', 'NULL',         1, '', '',
+],
       'primary_key' => 'svcpart',
       'unique' => [],
       'index' => [ [ 'disabled' ] ],
@@ -2257,6 +2302,7 @@ sub tables_hashref {
         'cgp_sendmdnmode',    'varchar', 'NULL', $char_d, '', '',#SendMDNMode
         #mail
         #XXX RPOP settings
+        #
       ],
       'primary_key' => 'svcnum',
       #'unique' => [ [ 'username', 'domsvc' ] ],
@@ -2683,9 +2729,10 @@ sub tables_hashref {
       'columns' => [
         'exportnum',   'serial',     '',      '', '', '', 
         'exportname', 'varchar', 'NULL', $char_d, '', '',
-        'machine',    'varchar', 'NULL', $char_d, '', '', 
+        'machine',    'varchar', 'NULL', $char_d, '', '',
         'exporttype', 'varchar',     '', $char_d, '', '', 
         'nodomain',      'char', 'NULL',       1, '', '', 
+        'default_machine','int', 'NULL',      '', '', '',
       ],
       'primary_key' => 'exportnum',
       'unique'      => [],
@@ -2862,22 +2909,28 @@ sub tables_hashref {
 
     'svc_broadband' => {
       'columns' => [
-        'svcnum',                  'int',     '',      '', '', '', 
-        'description',         'varchar', 'NULL', $char_d, '', '', 
-        'routernum',               'int', 'NULL',      '', '', '',
-        'blocknum',                'int', 'NULL',      '', '', '', 
-        'sectornum',               'int', 'NULL',      '', '', '',
-        'speed_up',                'int', 'NULL',      '', '', '', 
-        'speed_down',              'int', 'NULL',      '', '', '', 
-        'ip_addr',             'varchar', 'NULL',      15, '', '', 
-        'mac_addr',            'varchar', 'NULL',      12, '', '', 
-        'authkey',             'varchar', 'NULL',      32, '', '', 
-        'latitude',            'decimal', 'NULL',  '10,7', '', '', 
-        'longitude',           'decimal', 'NULL',  '10,7', '', '', 
-        'altitude',            'decimal', 'NULL',      '', '', '', 
-        'vlan_profile',        'varchar', 'NULL', $char_d, '', '', 
-        'performance_profile', 'varchar', 'NULL', $char_d, '', '',
-        'plan_id',             'varchar', 'NULL', $char_d, '', '',
+        'svcnum',                  'int',     '',        '', '', '', 
+        'description',         'varchar', 'NULL',   $char_d, '', '', 
+        'routernum',               'int', 'NULL',        '', '', '',
+        'blocknum',                'int', 'NULL',        '', '', '', 
+        'sectornum',               'int', 'NULL',        '', '', '',
+        'speed_up',                'int', 'NULL',        '', '', '', 
+        'speed_down',              'int', 'NULL',        '', '', '', 
+        'ip_addr',             'varchar', 'NULL',        15, '', '', 
+        'mac_addr',            'varchar', 'NULL',        12, '', '', 
+        'authkey',             'varchar', 'NULL',        32, '', '', 
+        'latitude',            'decimal', 'NULL',    '10,7', '', '', 
+        'longitude',           'decimal', 'NULL',    '10,7', '', '', 
+        'altitude',            'decimal', 'NULL',        '', '', '', 
+        'vlan_profile',        'varchar', 'NULL',   $char_d, '', '', 
+        'performance_profile', 'varchar', 'NULL',   $char_d, '', '',
+        'plan_id',             'varchar', 'NULL',   $char_d, '', '',
+        'radio_serialnum',     'varchar', 'NULL',   $char_d, '', '',
+        'radio_location',      'varchar', 'NULL', 2*$char_d, '', '',
+        'poe_location',        'varchar', 'NULL', 2*$char_d, '', '',
+        'rssi',                    'int', 'NULL',        '', '', '',
+        'suid',                    'int', 'NULL',        '', '', '',
+        'shared_svcnum',           'int', 'NULL',        '', '', '',
       ],
       'primary_key' => 'svcnum',
       'unique'      => [ [ 'ip_addr' ], [ 'mac_addr' ] ],
@@ -3016,6 +3069,32 @@ sub tables_hashref {
       'index' => [ [ 'disabled' ] ],
     },
 
+    'part_pkg_usage' => {
+      'columns' => [
+        'pkgusagepart', 'serial',   '', '', '', '',
+        'pkgpart',  'int',      '', '', '', '',
+        'minutes',  'int',      '', '', '', '',
+        'priority', 'int',  'NULL', '', '', '',
+        'shared',   'char', 'NULL',  1, '', '',
+        'rollover', 'char', 'NULL',  1, '', '',
+        'description',  'varchar', 'NULL', $char_d, '', '',
+      ],
+      'primary_key' => 'pkgusagepart',
+      'unique'      => [],
+      'index'       => [ [ 'pkgpart' ] ],
+    },
+
+    'part_pkg_usage_class' => {
+      'columns' => [
+        'num',       'serial',  '', '', '', '',
+        'pkgusagepart', 'int',  '', '', '', '',
+        'classnum',     'int','NULL', '', '', '',
+      ],
+      'primary_key' => 'num',
+      'unique'      => [ [ 'pkgusagepart', 'classnum' ] ],
+      'index'       => [],
+    },
+
     'rate' => {
       'columns' => [
         'ratenum',  'serial', '', '', '', '', 
@@ -3053,6 +3132,7 @@ sub tables_hashref {
       'columns' => [
         'regionnum',   'serial',      '', '', '', '', 
         'regionname',  'varchar',     '', $char_d, '', '', 
+        'exact_match', 'char',    'NULL',  1, '', '',
       ],
       'primary_key' => 'regionnum',
       'unique'      => [],
@@ -3333,6 +3413,12 @@ sub tables_hashref {
         'quantity',                'int', 'NULL',      '', '', '', 
 
         'upstream_rateid',         'int', 'NULL',      '', '', '',
+
+        ###
+        # more fields, for GSM imports
+        ###
+        'servicecode',             'int', 'NULL',      '', '', '',
+        'quantity_able',           'int', 'NULL',      '', '', '', 
         
         ###
         #and now for our own fields
@@ -3341,8 +3427,9 @@ sub tables_hashref {
         'cdrtypenum',              'int', 'NULL',      '', '', '',
 
         'charged_party',       'varchar', 'NULL', $char_d, '', '',
+        'charged_party_imsi',  'varchar', 'NULL', $char_d, '', '',
 
-        'upstream_price',      'decimal', 'NULL',  '10,4', '', '', 
+        'upstream_price',      'decimal', 'NULL',  '10,5', '', '', 
         'upstream_src_regionname', 'varchar', 'NULL', $char_d, '', '',
         'upstream_dst_regionname', 'varchar', 'NULL', $char_d, '', '',
 
@@ -3357,7 +3444,7 @@ sub tables_hashref {
         'rated_classnum',             'int', 'NULL',      '', '', '', 
         'rated_ratename',         'varchar', 'NULL', $char_d, '', '', 
 
-        'carrierid',               'int', 'NULL',      '', '', '',
+        'carrierid',               'bigint', 'NULL',      '', '', '',
 
         # service it was matched to
         'svcnum',             'int',   'NULL',     '',   '', '', 
@@ -3590,7 +3677,8 @@ sub tables_hashref {
       'columns' => [
         'svcnum',       'int',         '',      '', '', '', 
         'countrycode',  'varchar',     '',       3, '', '', 
-        'phonenum',     'varchar',     '',      15, '', '',  #12 ?
+        'phonenum',     'varchar',     '',      25, '', '',  #12 ?
+        'sim_imsi',     'varchar', 'NULL',      15, '', '',
         'pin',          'varchar', 'NULL', $char_d, '', '',
         'sip_password', 'varchar', 'NULL', $char_d, '', '',
         'phone_name',   'varchar', 'NULL', $char_d, '', '',
index 6d7ea26..8b0e16a 100644 (file)
@@ -52,10 +52,10 @@ line item, and for generic taxes, simply returns "Tax".
 =cut
 
 sub desc {
-  my $self = shift;
+  my( $self, $locale ) = @_;
 
   if ( $self->pkgnum > 0 ) {
-    $self->itemdesc || $self->part_pkg->pkg;
+    $self->itemdesc || $self->part_pkg->pkg_locale($locale);
   } else {
     my $desc = $self->itemdesc || 'Tax';
     $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
@@ -271,10 +271,12 @@ sub cust_bill_pkg_display {
   } else {
     my $hashref = { 'billpkgnum' => $self->billpkgnum };
     $hashref->{type} = $type if defined($type);
+
+    my $order_by = $self->display_table_orderby || 'billpkgdisplaynum';
     
     @result = qsearch ({ 'table'    => $self->display_table,
-                         'hashref'  => { 'billpkgnum' => $self->billpkgnum },
-                         'order_by' => 'ORDER BY billpkgdisplaynum',
+                         'hashref'  => $hashref,
+                         'order_by' => "ORDER BY $order_by",
                       });
   }
 
index adab9d5..2e78f12 100644 (file)
@@ -122,7 +122,9 @@ sub print_latex {
     UNLINK   => 0,
   ) or die "can't open temp file: $!\n";
 
-  my $agentnum = $self->cust_main->agentnum;
+  my $cust_main = $self->cust_main;
+  my $prospect_main = $self->prospect_main;
+  my $agentnum = $cust_main ? $cust_main->agentnum : $prospect_main->agentnum;
 
   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
@@ -363,14 +365,6 @@ sub print_generic {
 
   my $date_format = $date_formats{$format};
 
-  my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
-                                               },
-                             'html'     => sub { return '<b>'. shift(). '</b>'
-                                               },
-                             'template' => sub { shift },
-                           );
-  my $embolden_function = $embolden_functions{$format};
-
   my %newline_tokens = (  'latex'     => '\\\\',
                           'html'      => '<br>',
                           'template'  => "\n",
@@ -584,16 +578,20 @@ sub print_generic {
   #my $balance_due = $self->owed + $pr_total - $cr_total;
   my $balance_due = $self->owed + $pr_total;
 
-  # the customer's current balance as shown on the invoice before this one
-  $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
+  #these are used on the summary page only
+
+    # the customer's current balance as shown on the invoice before this one
+    $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
 
-  # the change in balance from that invoice to this one
-  $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
+    # the change in balance from that invoice to this one
+    $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
 
-  # the sum of amount owed on all previous invoices
-  $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
+    # the sum of amount owed on all previous invoices
+    # ($pr_total is used elsewhere but not as $previous_balance)
+    $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
 
   # the sum of amount owed on all invoices
+  # (this is used in the summary & on the payment coupon)
   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
 
   # info from customer's last invoice before this one, for some 
@@ -727,10 +725,11 @@ sub print_generic {
 
 
   my $adjusttotal = 0;
-  my $adjust_section = { 'description' => 
-    $self->mt('Credits, Payments, and Adjustments'),
-                         'subtotal'    => 0,   # adjusted below
-                       };
+  my $adjust_section = {
+    'description'    => $self->mt('Credits, Payments, and Adjustments'),
+    'adjust_section' => 1,
+    'subtotal'       => 0,   # adjusted below
+  };
   my $adjust_weight = _pkg_category($adjust_section->{description})
                         ? _pkg_category($adjust_section->{description})->weight
                         : 0;
@@ -738,7 +737,7 @@ sub print_generic {
   $adjust_section->{'sort_weight'} = $adjust_weight;
 
   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
-  my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
+  my $multisection = $conf->exists($tc.'_sections', $cust_main->agentnum);
   $invoice_data{'multisection'} = $multisection;
   my $late_sections = [];
   my $extra_sections = [];
@@ -936,6 +935,7 @@ sub print_generic {
       $detail->{'sdate'} = $line_item->{'sdate'};
       $detail->{'edate'} = $line_item->{'edate'};
       $detail->{'seconds'} = $line_item->{'seconds'};
+      $detail->{'svc_label'} = $line_item->{'svc_label'};
   
       push @detail_items, $detail;
       push @buf, ( [ $detail->{'description'},
@@ -1033,9 +1033,33 @@ sub print_generic {
              $money_char. sprintf("%10.2f",$self->charged) ];
   push @buf,['',''];
 
-  # calculate total, possibly including total owed on previous
-  # invoices
-  {
+
+  ###
+  # Totals
+  ###
+
+  my %embolden_functions = (
+    'latex'    => sub { return '\textbf{'. shift(). '}' },
+    'html'     => sub { return '<b>'. shift(). '</b>' },
+    'template' => sub { shift },
+  );
+  my $embolden_function = $embolden_functions{$format};
+
+  if ( $self->can('_items_total') ) { # quotations
+
+    $self->_items_total(\@total_items);
+
+    foreach ( @total_items ) {
+      $_->{'total_item'}   = &$embolden_function( $_->{'total_item'} );
+      $_->{'total_amount'} = &$embolden_function( $other_money_char.
+                                                   $_->{'total_amount'}
+                                                );
+    }
+
+  } else { #normal invoice case
+
+    # calculate total, possibly including total owed on previous
+    # invoices
     my $total = {};
     my $item = 'Total';
     $item = $conf->config('previous_balance-exclude_from_total')
@@ -1066,126 +1090,128 @@ sub print_generic {
                sprintf( '%10.2f', $amount )
               ];
     push @buf,['',''];
-  }
 
-  # if we're showing previous invoices, also show previous
-  # credits and payments 
-  if ( $self->enable_previous 
-        and $self->can('_items_credits')
-        and $self->can('_items_payments') )
-    {
-    #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
-  
-    # credits
-    my $credittotal = 0;
-    foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
+    # if we're showing previous invoices, also show previous
+    # credits and payments 
+    if ( $self->enable_previous 
+          and $self->can('_items_credits')
+          and $self->can('_items_payments') )
+      {
+      #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
+    
+      # credits
+      my $credittotal = 0;
+      foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
+
+        my $total;
+        $total->{'total_item'} = &$escape_function($credit->{'description'});
+        $credittotal += $credit->{'amount'};
+        $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
+        $adjusttotal += $credit->{'amount'};
+        if ( $multisection ) {
+          my $money = $old_latex ? '' : $money_char;
+          push @detail_items, {
+            ext_description => [],
+            ref          => '',
+            quantity     => '',
+            description  => &$escape_function($credit->{'description'}),
+            amount       => $money. $credit->{'amount'},
+            product_code => '',
+            section      => $adjust_section,
+          };
+        } else {
+          push @total_items, $total;
+        }
 
-      my $total;
-      $total->{'total_item'} = &$escape_function($credit->{'description'});
-      $credittotal += $credit->{'amount'};
-      $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
-      $adjusttotal += $credit->{'amount'};
-      if ( $multisection ) {
-        my $money = $old_latex ? '' : $money_char;
-        push @detail_items, {
-          ext_description => [],
-          ref          => '',
-          quantity     => '',
-          description  => &$escape_function($credit->{'description'}),
-          amount       => $money. $credit->{'amount'},
-          product_code => '',
-          section      => $adjust_section,
-        };
-      } else {
-        push @total_items, $total;
       }
+      $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
 
-    }
-    $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
-
-    #credits (again)
-    foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
-      push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
-    }
+      #credits (again)
+      foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
+        push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
+      }
 
-    # payments
-    my $paymenttotal = 0;
-    foreach my $payment ( $self->_items_payments ) {
-      my $total = {};
-      $total->{'total_item'} = &$escape_function($payment->{'description'});
-      $paymenttotal += $payment->{'amount'};
-      $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
-      $adjusttotal += $payment->{'amount'};
+      # payments
+      my $paymenttotal = 0;
+      foreach my $payment ( $self->_items_payments ) {
+        my $total = {};
+        $total->{'total_item'} = &$escape_function($payment->{'description'});
+        $paymenttotal += $payment->{'amount'};
+        $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
+        $adjusttotal += $payment->{'amount'};
+        if ( $multisection ) {
+          my $money = $old_latex ? '' : $money_char;
+          push @detail_items, {
+            ext_description => [],
+            ref          => '',
+            quantity     => '',
+            description  => &$escape_function($payment->{'description'}),
+            amount       => $money. $payment->{'amount'},
+            product_code => '',
+            section      => $adjust_section,
+          };
+        }else{
+          push @total_items, $total;
+        }
+        push @buf, [ $payment->{'description'},
+                     $money_char. sprintf("%10.2f", $payment->{'amount'}),
+                   ];
+      }
+      $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
+    
       if ( $multisection ) {
-        my $money = $old_latex ? '' : $money_char;
-        push @detail_items, {
-          ext_description => [],
-          ref          => '',
-          quantity     => '',
-          description  => &$escape_function($payment->{'description'}),
-          amount       => $money. $payment->{'amount'},
-          product_code => '',
-          section      => $adjust_section,
-        };
-      }else{
-        push @total_items, $total;
+        $adjust_section->{'subtotal'} = $other_money_char.
+                                        sprintf('%.2f', $adjusttotal);
+        push @sections, $adjust_section
+          unless $adjust_section->{sort_weight};
       }
-      push @buf, [ $payment->{'description'},
-                   $money_char. sprintf("%10.2f", $payment->{'amount'}),
-                 ];
-    }
-    $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
-  
-    if ( $multisection ) {
-      $adjust_section->{'subtotal'} = $other_money_char.
-                                      sprintf('%.2f', $adjusttotal);
-      push @sections, $adjust_section
-        unless $adjust_section->{sort_weight};
-    }
 
-    # create Balance Due message
-    { 
-      my $total;
-      $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
-      $total->{'total_amount'} =
-        &$embolden_function(
-          $other_money_char. sprintf('%.2f', $summarypage 
-                                               ? $self->charged +
-                                                 $self->billing_balance
-                                               : $self->owed + $pr_total
-                                    )
-        );
-      if ( $multisection && !$adjust_section->{sort_weight} ) {
-        $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
-                                         $total->{'total_amount'};
-      }else{
-        push @total_items, $total;
+      # create Balance Due message
+      { 
+        my $total;
+        $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
+        $total->{'total_amount'} =
+          &$embolden_function(
+            $other_money_char. sprintf('%.2f', #why? $summarypage 
+                                               #  ? $self->charged +
+                                               #    $self->billing_balance
+                                               #  :
+                                                   $self->owed + $pr_total
+                                      )
+          );
+        if ( $multisection && !$adjust_section->{sort_weight} ) {
+          $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
+                                           $total->{'total_amount'};
+        }else{
+          push @total_items, $total;
+        }
+        push @buf,['','-----------'];
+        push @buf,[$self->balance_due_msg, $money_char. 
+          sprintf("%10.2f", $balance_due ) ];
       }
-      push @buf,['','-----------'];
-      push @buf,[$self->balance_due_msg, $money_char. 
-        sprintf("%10.2f", $balance_due ) ];
-    }
 
-    if ( $conf->exists('previous_balance-show_credit')
-        and $cust_main->balance < 0 ) {
-      my $credit_total = {
-        'total_item'    => &$embolden_function($self->credit_balance_msg),
-        'total_amount'  => &$embolden_function(
-          $other_money_char. sprintf('%.2f', -$cust_main->balance)
-        ),
-      };
-      if ( $multisection ) {
-        $adjust_section->{'posttotal'} .= $newline_token .
-          $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
-      }
-      else {
-        push @total_items, $credit_total;
+      if ( $conf->exists('previous_balance-show_credit')
+          and $cust_main->balance < 0 ) {
+        my $credit_total = {
+          'total_item'    => &$embolden_function($self->credit_balance_msg),
+          'total_amount'  => &$embolden_function(
+            $other_money_char. sprintf('%.2f', -$cust_main->balance)
+          ),
+        };
+        if ( $multisection ) {
+          $adjust_section->{'posttotal'} .= $newline_token .
+            $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
+        }
+        else {
+          push @total_items, $credit_total;
+        }
+        push @buf,['','-----------'];
+        push @buf,[$self->credit_balance_msg, $money_char. 
+          sprintf("%10.2f", -$cust_main->balance ) ];
       }
-      push @buf,['','-----------'];
-      push @buf,[$self->credit_balance_msg, $money_char. 
-        sprintf("%10.2f", -$cust_main->balance ) ];
     }
-  }
+
+  } #end of default total adding ! can('_items_total')
 
   if ( $multisection ) {
     if (    $conf->exists('svc_phone_sections')
@@ -2042,6 +2068,11 @@ separate quantities, for some reason).
 
 =cut
 
+sub _items_nontax {
+  my $self = shift;
+  grep { $_->pkgnum } $self->cust_bill_pkg;
+}
+
 sub _items_pkg {
   my $self = shift;
   my %options = @_;
@@ -2049,7 +2080,7 @@ sub _items_pkg {
   warn "$me _items_pkg searching for all package line items\n"
     if $DEBUG > 1;
 
-  my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
+  my @cust_bill_pkg = $self->_items_nontax;
 
   warn "$me _items_pkg filtering line items\n"
     if $DEBUG > 1;
@@ -2150,6 +2181,7 @@ sub _items_cust_bill_pkg {
 
   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
                                    # and location labels
+  my $locale = $cust_main->locale;
 
   my @b = ();
   my ($s, $r, $u) = ( undef, undef, undef );
@@ -2194,7 +2226,7 @@ sub _items_cust_bill_pkg {
 
       my $type = $display->type;
 
-      my $desc = $cust_bill_pkg->desc;
+      my $desc = $cust_bill_pkg->desc( $cust_main->locale );
       $desc = substr($desc, 0, $maxlength). '...'
         if $format eq 'latex' && length($desc) > $maxlength;
 
@@ -2260,13 +2292,16 @@ sub _items_cust_bill_pkg {
             || $cust_bill_pkg->recur_show_zero;
 
           my @d = ();
+          my $svc_label;
           unless ( $cust_pkg->part_pkg->hide_svc_detail
                 || $cust_bill_pkg->hidden )
           {
 
-            push @d, map &{$escape_function}($_),
-                         $cust_pkg->h_labels_short($self->_date, undef, 'I')
+            my @svc_labels = map &{$escape_function}($_),
+                        $cust_pkg->h_labels_short($self->_date, undef, 'I');
+            push @d, @svc_labels
               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+            $svc_label = $svc_labels[0];
 
             if ( ! $cust_pkg->locationnum or
                    $cust_pkg->locationnum != $cust_main->ship_locationnum  ) {
@@ -2296,6 +2331,7 @@ sub _items_cust_bill_pkg {
               unit_amount     => $cust_bill_pkg->unitsetup,
               quantity        => $cust_bill_pkg->quantity,
               ext_description => \@d,
+              svc_label       => ($svc_label || ''),
             };
           };
 
@@ -2318,16 +2354,25 @@ sub _items_cust_bill_pkg {
           my $description = ($is_summary && $type && $type eq 'U')
                             ? "Usage charges" : $desc;
 
+          my $part_pkg = $cust_pkg->part_pkg;
+
           #pry be a bit more efficient to look some of this conf stuff up
           # outside the loop
           unless (
             $conf->exists('disable_line_item_date_ranges')
-              || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
+              || $part_pkg->option('disable_line_item_date_ranges',1)
+              || ! $cust_bill_pkg->sdate
+              || ! $cust_bill_pkg->edate
           ) {
             my $time_period;
-            my $date_style = $conf->config( 'cust_bill-line_item-date_style',
+            my $date_style = '';
+            $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
+                                         $cust_main->agentnum
+                                       )
+              if $part_pkg && $part_pkg->freq !~ /^1m?$/;
+            $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
                                             $cust_main->agentnum
-                                          );
+                                         );
             if ( defined($date_style) && $date_style eq 'month_of' ) {
               $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
             } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
@@ -2345,6 +2390,7 @@ sub _items_cust_bill_pkg {
 
           my @d = ();
           my @seconds = (); # for display of usage info
+          my $svc_label = '';
 
           #at least until cust_bill_pkg has "past" ranges in addition to
           #the "future" sdate/edate ones... see #3032
@@ -2353,7 +2399,7 @@ sub _items_cust_bill_pkg {
           push @dates, $prev->sdate if $prev;
           push @dates, undef if !$prev;
 
-          unless ( $cust_pkg->part_pkg->hide_svc_detail
+          unless ( $part_pkg->hide_svc_detail
                 || $cust_bill_pkg->itemdesc
                 || $cust_bill_pkg->hidden
                 || $is_summary && $type && $type eq 'U'
@@ -2363,11 +2409,11 @@ sub _items_cust_bill_pkg {
             warn "$me _items_cust_bill_pkg adding service details\n"
               if $DEBUG > 1;
 
-            push @d, map &{$escape_function}($_),
-                         $cust_pkg->h_labels_short(@dates, 'I')
-                                                   #$cust_bill_pkg->edate,
-                                                   #$cust_bill_pkg->sdate)
+            my @svc_labels = map &{$escape_function}($_),
+                        $cust_pkg->h_labels_short($self->_date, undef, 'I');
+            push @d, @svc_labels
               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+            $svc_label = $svc_labels[0];
 
             warn "$me _items_cust_bill_pkg done adding service details\n"
               if $DEBUG > 1;
@@ -2450,6 +2496,7 @@ sub _items_cust_bill_pkg {
                 quantity        => $cust_bill_pkg->quantity,
                 %item_dates,
                 ext_description => \@d,
+                svc_label       => ($svc_label || ''),
               };
               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
             }
index c2ea0a6..c8ad430 100644 (file)
@@ -6,7 +6,7 @@ use Exporter;
 use Carp qw( confess );
 use HTML::Entities;
 use FS::Conf;
-use FS::Misc::DateTime qw( parse_datetime );
+use FS::Misc::DateTime qw( parse_datetime day_end );
 use FS::Record qw(dbdef);
 use FS::cust_main;  # are sql_balance and sql_date_balance in the right module?
 
@@ -32,16 +32,16 @@ sub parse_beginning_ending {
   my $beginning = 0;
   if ( $cgi->param($prefix.'begin') =~ /^(\d+)$/ ) {
     $beginning = $1;
-  } elsif ( $cgi->param($prefix.'beginning') =~ /^([ 0-9\-\/]{1,64})$/ ) {
+  } elsif ( $cgi->param($prefix.'beginning') =~ /^([ 0-9\-\/\:]{1,64})$/ ) {
     $beginning = parse_datetime($1) || 0;
   }
 
   my $ending = 4294967295; #2^32-1
   if ( $cgi->param($prefix.'end') =~ /^(\d+)$/ ) {
     $ending = $1 - 1;
-  } elsif ( $cgi->param($prefix.'ending') =~ /^([ 0-9\-\/]{1,64})$/ ) {
-    #probably need an option to turn off the + 86399
-    $ending = parse_datetime($1) + 86399;
+  } elsif ( $cgi->param($prefix.'ending') =~ /^([ 0-9\-\/\:]{1,64})$/ ) {
+    $ending = parse_datetime($1);
+    $ending = day_end($ending) unless $ending =~ /:/;
   }
 
   ( $beginning, $ending );
@@ -235,20 +235,20 @@ sub cust_header {
     '(service) Name'           => 'ship_contact',
     '(bill) Company'           => 'company',
     '(service) Company'        => 'ship_company',
-    'Address 1'                => 'address1',
-    'Address 2'                => 'address2',
-    'City'                     => 'city',
-    'State'                    => 'state',
-    'Zip'                      => 'zip',
+    'Address 1'                => 'bill_address1',
+    'Address 2'                => 'bill_address2',
+    'City'                     => 'bill_city',
+    'State'                    => 'bill_state',
+    'Zip'                      => 'bill_zip',
     'Country'                  => 'country_full',
     'Day phone'                => 'daytime', # XXX should use msgcat, but how?
     'Night phone'              => 'night',   # XXX should use msgcat, but how?
     'Fax number'               => 'fax',
-    '(bill) Address 1'         => 'address1',
-    '(bill) Address 2'         => 'address2',
-    '(bill) City'              => 'city',
-    '(bill) State'             => 'state',
-    '(bill) Zip'               => 'zip',
+    '(bill) Address 1'         => 'bill_address1',
+    '(bill) Address 2'         => 'bill_address2',
+    '(bill) City'              => 'bill_city',
+    '(bill) State'             => 'bill_state',
+    '(bill) Zip'               => 'bill_zip',
     '(bill) Country'           => 'country_full',
     '(bill) Day phone'         => 'daytime', # XXX should use msgcat, but how?
     '(bill) Night phone'       => 'night',   # XXX should use msgcat, but how?
@@ -335,17 +335,21 @@ setting is supplied, the <B>cust-fields</B> configuration value.
 sub cust_sql_fields {
 
   my @fields = qw( last first company );
-  push @fields, map "ship_$_", @fields;
-  push @fields, 'country';
+#  push @fields, map "ship_$_", @fields;
 
   cust_header(@_);
   #inefficientish, but tiny lists and only run once per page
 
-  my @add_fields = qw( address1 address2 city state zip daytime night fax );
-  push @fields,
-    grep { my $field = $_; grep { $_ eq $field } @cust_fields }
-         ( @add_fields, ( map "ship_$_", @add_fields ), 'payby' );
-
+  my @location_fields;
+  foreach my $field (qw( address1 address2 city state zip )) {
+    foreach my $pre ('bill_','ship_') {
+      if ( grep { $_ eq $pre.$field } @cust_fields ) {
+        push @location_fields, $pre.'location.'.$field.' AS '.$pre.$field;
+      }
+    }
+  }
+  
+  push @fields, 'payby' if grep { $_ eq 'payby'} @cust_fields;
   push @fields, 'agent_custid';
 
   my @extra_fields = ();
@@ -353,7 +357,71 @@ sub cust_sql_fields {
     push @extra_fields, FS::cust_main->balance_sql . " AS current_balance";
   }
 
-  map("cust_main.$_", @fields), @extra_fields;
+  map("cust_main.$_", @fields), @location_fields, @extra_fields;
+}
+
+=item join_cust_main [ TABLE[.CUSTNUM] ] [ LOCATION_TABLE[.LOCATIONNUM] ]
+
+Returns an SQL join phrase for the FROM clause so that the fields listed
+in L<cust_sql_fields> will be available.  Currently joins to cust_main 
+itself, as well as cust_location (under the aliases 'bill_location' and
+'ship_location') if address fields are needed.  L<cust_header()> should have
+been called already.
+
+All of these will be left joins; if you want to exclude rows with no linked
+cust_main record (or bill_location/ship_location), you can do so in the 
+WHERE clause.
+
+TABLE is the table containing the custnum field.  If CUSTNUM (a field name
+in that table) is specified, that field will be joined to cust_main.custnum.
+Otherwise, this function will assume the field is named "custnum".  If the 
+argument isn't present at all, the join will just say "USING (custnum)", 
+which might work.
+
+As a special case, if TABLE is 'cust_main', only the joins to cust_location
+will be returned.
+
+LOCATION_TABLE is an optional table name to use for joining ship_location,
+in case your query also includes package information and you want the 
+"service address" columns to reflect package addresses.
+
+=cut
+
+sub join_cust_main {
+  my ($cust_table, $location_table) = @_;
+  my ($custnum, $locationnum);
+  ($cust_table, $custnum) = split(/\./, $cust_table);
+  $custnum ||= 'custnum';
+  ($location_table, $locationnum) = split(/\./, $location_table);
+  $locationnum ||= 'locationnum';
+
+  my $sql = '';
+  if ( $cust_table ) {
+    $sql = " LEFT JOIN cust_main ON (cust_main.custnum = $cust_table.$custnum)"
+      unless $cust_table eq 'cust_main';
+  } else {
+    $sql = " LEFT JOIN cust_main USING (custnum)";
+  }
+
+  if ( !@cust_fields or grep /^bill_/, @cust_fields ) {
+
+    $sql .= ' LEFT JOIN cust_location bill_location'.
+            ' ON (bill_location.locationnum = cust_main.bill_locationnum)';
+
+  }
+
+  if ( !@cust_fields or grep /^ship_/, @cust_fields ) {
+
+    if (!$location_table) {
+      $location_table = 'cust_main';
+      $locationnum = 'ship_locationnum';
+    }
+
+    $sql .= ' LEFT JOIN cust_location ship_location'.
+            " ON (ship_location.locationnum = $location_table.$locationnum) ";
+  }
+
+  $sql;
 }
 
 =item cust_fields OBJECT [ CUST_FIELDS_VALUE ]
@@ -404,23 +472,26 @@ sub cust_fields_subs {
   my $unlinked_warn = 0;
   return map { 
     my $f = $_;
-    if( $unlinked_warn++ ) {
+    if ( $unlinked_warn++ ) {
+
       sub {
         my $record = shift;
-        if( $record->custnum ) {
-          $record->$f(@_);
-        }
-        else {
+        if ( $record->custnum ) {
+          encode_entities( $record->$f(@_) );
+        } else {
           '(unlinked)'
         };
-      }
-    } 
-    else {
+      };
+
+    } else {
+
       sub {
         my $record = shift;
-        $record->$f(@_) if $record->custnum;
-      }
+        $record->custnum ? encode_entities( $record->$f(@_) ) : '';
+      };
+
     }
+
   } @cust_fields;
 }
 
@@ -510,7 +581,7 @@ use vars qw($DEBUG);
 use Carp;
 use Storable qw(nfreeze);
 use MIME::Base64;
-use JSON;
+use JSON::XS;
 use FS::UID qw(getotaker);
 use FS::Record qw(qsearchs);
 use FS::queue;
@@ -655,10 +726,7 @@ sub job_status {
     @return = ( 'error', $job ? $job->statustext : $jobnum );
   }
 
-  #to_json(\@return);  #waiting on deb 5.0 for new JSON.pm?
-  #silence the warning though
-  my $to_json = JSON->can('to_json') || JSON->can('objToJson');
-  &$to_json(\@return);
+  encode_json \@return;
 
 }
 
index fea53a2..cda3198 100644 (file)
@@ -294,6 +294,9 @@ sub upgrade_data {
     #insert default tower_sector if not present
     'tower' => [],
 
+    #repair improperly deleted services
+    'cust_svc' => [],
+
     #routernum/blocknum
     'svc_broadband' => [],
 
index 397b456..d370ba5 100644 (file)
@@ -198,6 +198,10 @@ sub _upgrade_data { # class method
     'New prospect'                        => 'Generate quotation',
     'Delete invoices'                     => 'Void invoices',
     'List invoices'                       => 'List quotations',
+    'Post credit'                         => 'Credit line items',
+    #'View customer tax exemptions'        => 'Edit customer tax exemptions',
+    'Edit customer'                       => 'Edit customer tax exemptions',
+    'Edit package definitions'            => 'Bulk edit package definitions',
 
     'List services'    => [ 'Services: Accounts',
                             'Services: Domains',
@@ -218,12 +222,17 @@ sub _upgrade_data { # class method
     'Services: Accounts' => 'Services: Accounts: Advanced search',
     'Services: Wireless broadband services' => 'Services: Wireless broadband services: Advanced search',
     'Services: Hardware' => 'Services: Hardware: Advanced search',
+    'Services: Phone numbers' => 'Services: Phone numbers: Advanced search',
 
     'List rating data' => [ 'Usage: RADIUS sessions',
                             'Usage: Call Detail Records (CDRs)',
                             'Usage: Unrateable CDRs',
                           ],
-  ;
+    'Provision customer service' => [ 'Edit password' ],
+    'Financial reports' => [ 'Employees: Commission Report',
+                             'Employees: Audit Report',
+                           ],
+;
 
   foreach my $old_acl ( keys %onetime ) {
 
index fdec921..3ebe6c4 100644 (file)
@@ -11,6 +11,7 @@ use Date::Parse;
 use Date::Format;
 use Time::Local;
 use List::Util qw( first min );
+use Text::CSV_XS;
 use FS::UID qw( dbh );
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs );
@@ -325,6 +326,10 @@ sub check {
     $self->billsec(  $self->enddate - $self->answerdate );
   } 
 
+  if ( ! $self->enddate && $self->startdate && $self->duration ) {
+    $self->enddate( $self->startdate + $self->duration );
+  }
+
   $self->set_charged_party;
 
   #check the foreign keys even?
@@ -421,12 +426,25 @@ sub set_charged_party {
 Sets the status to the provided string.  If there is an error, returns the
 error, otherwise returns false.
 
+If status is being changed from 'rated' to some other status, also removes
+any usage allocations to this CDR.
+
 =cut
 
 sub set_status {
   my($self, $status) = @_;
+  my $old_status = $self->freesidestatus;
   $self->freesidestatus($status);
-  $self->replace;
+  my $error = $self->replace;
+  if ( $old_status eq 'rated' and $status ne 'done' ) {
+    # deallocate any usage
+    foreach (qsearch('cdr_cust_pkg_usage', {acctid => $self->acctid})) {
+      my $cust_pkg_usage = $_->cust_pkg_usage;
+      $cust_pkg_usage->set('minutes', $cust_pkg_usage->minutes + $_->minutes);
+      $error ||= $cust_pkg_usage->replace || $_->delete;
+    }
+  }
+  $error;
 }
 
 =item set_status_and_rated_price STATUS RATED_PRICE [ SVCNUM [ OPTION => VALUE ... ] ]
@@ -573,7 +591,7 @@ reference of the number of included minutes and will be decremented by the
 rated minutes of this CDR.
 
 region_group_included_minutes_hashref is required for prefix price plans which
-have included minues (otehrwise unused/ignored).  It should be set to an empty
+have included minues (otherwise unused/ignored).  It should be set to an empty
 hashref at the start of a month's rating and then preserved across CDRs.
 
 =cut
@@ -598,6 +616,7 @@ our %interval_cache = (); # for timed rates
 sub rate_prefix {
   my( $self, %opt ) = @_;
   my $part_pkg = $opt{'part_pkg'} or return "No part_pkg specified";
+  my $cust_pkg = $opt{'cust_pkg'};
 
   my $da_rewrote = 0;
   # this will result in those CDRs being marked as done... is that 
@@ -625,7 +644,34 @@ sub rate_prefix {
                                             );
   }
 
+  if ( $part_pkg->option_cacheable('skip_same_customer')
+      and ! $self->is_tollfree ) {
+    my ($dst_countrycode, $dst_number) = $self->parse_number(
+      column => 'dst',
+      international_prefix => $part_pkg->option_cacheable('international_prefix'),
+      domestic_prefix => $part_pkg->option_cacheable('domestic_prefix'),
+    );
+    my $dst_same_cust = FS::Record->scalar_sql(
+        'SELECT COUNT(svc_phone.svcnum) AS count '.
+        'FROM cust_pkg ' .
+        'JOIN cust_svc   USING (pkgnum) ' .
+        'JOIN svc_phone  USING (svcnum) ' .
+        'WHERE svc_phone.countrycode = ' . dbh->quote($dst_countrycode) .
+        ' AND svc_phone.phonenum = ' . dbh->quote($dst_number) .
+        ' AND cust_pkg.custnum = ' . $cust_pkg->custnum,
+    );
+    if ( $dst_same_cust > 0 ) {
+      warn "not charging for CDR (same source and destination customer)\n" if $DEBUG;
+      return $self->set_status_and_rated_price( 'skipped',
+                                                0,
+                                                $opt{'svcnum'},
+                                              );
+    }
+  }
+
     
+
+
   ###
   # look up rate details based on called station id
   # (or calling station id for toll free calls)
@@ -823,11 +869,6 @@ sub rate_prefix {
 
     $seconds_left -= $charge_sec;
 
-    my $included_min = $opt{'region_group_included_min_hashref'} || {};
-
-    $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included
-      unless exists $included_min->{$regionnum}{$ratetimenum};
-
     my $granularity = $rate_detail->sec_granularity;
 
     my $minutes;
@@ -845,20 +886,40 @@ sub rate_prefix {
 
     $seconds += $charge_sec;
 
+    if ( $rate_detail->min_included ) {
+      # the old, kind of deprecated way to do this
+      my $included_min = $opt{'region_group_included_min_hashref'} || {};
 
-    my $region_group = ($part_pkg->option_cacheable('min_included') || 0) > 0;
+      # by default, set the included minutes for this region/time to
+      # what's in the rate_detail
+      $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included
+        unless exists $included_min->{$regionnum}{$ratetimenum};
 
-    ${$opt{region_group_included_min}} -= $minutes 
-        if $region_group && $rate_detail->region_group;
+      # the way that doesn't work
+      #my $region_group = ($part_pkg->option_cacheable('min_included') || 0) > 0;
+
+      #${$opt{region_group_included_min}} -= $minutes 
+      #    if $region_group && $rate_detail->region_group;
+
+      if ( $included_min->{$regionnum}{$ratetimenum} > $minutes ) {
+        $charge_sec = 0;
+        $included_min->{$regionnum}{$ratetimenum} -= $minutes;
+      } else {
+        $charge_sec -= ($included_min->{$regionnum}{$ratetimenum} * 60);
+        $included_min->{$regionnum}{$ratetimenum} = 0;
+      }
+    } else {
+      # the new way!
+      my $applied_min = $cust_pkg->apply_usage(
+        'cdr'         => $self,
+        'rate_detail' => $rate_detail,
+        'minutes'     => $minutes
+      );
+      # for now, usage pools deal only in whole minutes
+      $charge_sec -= $applied_min * 60;
+    }
 
-    $included_min->{$regionnum}{$ratetimenum} -= $minutes;
-    if (
-         $included_min->{$regionnum}{$ratetimenum} <= 0
-         && ( ${$opt{region_group_included_min}} <= 0
-              || ! $rate_detail->region_group
-            )
-       )
-    {
+    if ( $charge_sec > 0 ) {
 
       #NOW do connection charges here... right?
       #my $conn_seconds = min($seconds_left, $rate_detail->conn_sec);
@@ -871,16 +932,9 @@ sub rate_prefix {
       }
 
                            #should preserve (display?) this
-      my $charge_min = 0 - $included_min->{$regionnum}{$ratetimenum} - ( $conn_seconds / 60 );
-      $included_min->{$regionnum}{$ratetimenum} = 0;
+      my $charge_min = ( $charge_sec - $conn_seconds ) / 60;
       $charge += ($rate_detail->min_charge * $charge_min) if $charge_min > 0; #still not rounded
 
-    } elsif ( ${$opt{region_group_included_min}} > 0
-              && $region_group
-              && $rate_detail->region_group 
-           )
-    {
-        $included_min->{$regionnum}{$ratetimenum} = 0 
     }
 
     # choose next rate_detail
@@ -1168,6 +1222,8 @@ sub export_formats {
     length($price) ? ($opt{money_char} . $price) : '';
   };
 
+  my $src_sub = sub { $_[0]->clid || $_[0]->src };
+
   %export_formats = (
     'simple' => [
       sub { time2str($date_format, shift->calldate_unix ) },   #DATE
@@ -1182,7 +1238,7 @@ sub export_formats {
       sub { time2str($date_format, shift->calldate_unix ) },   #DATE
       sub { time2str('%r', shift->calldate_unix ) },   #TIME
       #'userfield',                                     #USER
-      'src',                                           #called from
+      $src_sub,                                           #called from
       'dst',                                           #NUMBER_DIALED
       $duration_sub,                                   #DURATION
       #sub { sprintf('%.3f', shift->upstream_price ) }, #PRICE
@@ -1191,7 +1247,7 @@ sub export_formats {
     'accountcode_simple' => [
       sub { time2str($date_format, shift->calldate_unix ) },   #DATE
       sub { time2str('%r', shift->calldate_unix ) },   #TIME
-      'src',                                           #called from
+      $src_sub,                                           #called from
       'accountcode',                                   #NUMBER_DIALED
       $duration_sub,                                   #DURATION
       $price_sub,
@@ -1199,14 +1255,14 @@ sub export_formats {
     'sum_duration' => [ 
       # for summary formats, the CDR is a fictitious object containing the 
       # total billsec and the phone number of the service
-      'src',
+      $src_sub,
       sub { my($cdr, %opt) = @_; $opt{ratename} },
       sub { my($cdr, %opt) = @_; $opt{count} },
       sub { my($cdr, %opt) = @_; int($opt{seconds}/60).'m' },
       $price_sub,
     ],
     'sum_count' => [
-      'src',
+      $src_sub,
       sub { my($cdr, %opt) = @_; $opt{ratename} },
       sub { my($cdr, %opt) = @_; $opt{count} },
       $price_sub,
@@ -1240,7 +1296,7 @@ sub export_formats {
       $price_sub,
     ],
   );
-  $export_formats{'source_default'} = [ 'src', @{ $export_formats{'default'} }, ];
+  $export_formats{'source_default'} = [ $src_sub, @{ $export_formats{'default'} }, ];
   $export_formats{'accountcode_default'} =
     [ @{ $export_formats{'default'} }[0,1],
       'accountcode',
@@ -1248,7 +1304,7 @@ sub export_formats {
     ];
   my @default = @{ $export_formats{'default'} };
   $export_formats{'description_default'} = 
-    [ 'src', @default[0..2], 
+    [ $src_sub, @default[0..2], 
       sub { my($cdr, %opt) = @_; $cdr->description },
       @default[4,5] ];
 
@@ -1286,8 +1342,6 @@ sub downstream_csv {
   #$opt{'money_char'} ||= $conf->config('money_char') || '$';
   $opt{'money_char'} ||= FS::Conf->new->config('money_char') || '$';
 
-  eval "use Text::CSV_XS;";
-  die $@ if $@;
   my $csv = new Text::CSV_XS;
 
   my @columns =
@@ -1578,6 +1632,11 @@ my %import_options = (
           keys %cdr_info
     },
 
+  'format_asn_formats' =>
+    { map { $_ => $cdr_info{$_}->{'asn_format'}; }
+          keys %cdr_info
+    },
+
   'format_row_callbacks' => { map { $_ => $cdr_info{$_}->{'row_callback'}; }
                                   keys %cdr_info
                             },
diff --git a/FS/FS/cdr/asterisk_skip_clid.pm b/FS/FS/cdr/asterisk_skip_clid.pm
new file mode 100644 (file)
index 0000000..1a105b3
--- /dev/null
@@ -0,0 +1,45 @@
+package FS::cdr::asterisk_skip_clid;
+
+use strict;
+use vars qw(@ISA %info);
+use FS::cdr qw(_cdr_date_parser_maker);
+
+@ISA = qw(FS::cdr);
+
+#http://www.the-asterisk-book.com/unstable/funktionen-cdr.html
+my %amaflags = (
+  DEFAULT       => 0,
+  OMIT          => 1, #asterisk 1.4+
+  IGNORE        => 1, #asterisk 1.2
+  BILLING       => 2, #asterisk 1.4+
+  BILL          => 2, #asterisk 1.2
+  DOCUMENTATION => 3,
+  #? '' => 0,
+);
+
+%info = (
+  'name'          => 'Asterisk (skip Caller ID)',
+  'weight'        => 11,
+  'import_fields' => [
+    'accountcode',
+    'src',
+    'dst',
+    'dcontext',
+    'SKIP_clid',
+    'channel',
+    'dstchannel',
+    'lastapp',
+    'lastdata',
+    _cdr_date_parser_maker('startdate'),
+    _cdr_date_parser_maker('answerdate'),
+    _cdr_date_parser_maker('enddate'),
+    'duration',
+    'billsec',
+    'disposition',
+    sub { my($cdr, $amaflags) = @_; $cdr->amaflags($amaflags{$amaflags}); },
+    'uniqueid',
+    'userfield',
+  ],
+);
+
+1;
diff --git a/FS/FS/cdr/gsm_tap3_12.pm b/FS/FS/cdr/gsm_tap3_12.pm
new file mode 100644 (file)
index 0000000..275e7b3
--- /dev/null
@@ -0,0 +1,2079 @@
+package FS::cdr::gsm_tap3_12;
+use base qw( FS::cdr );
+
+use strict;
+use vars qw( %info %TZ );
+use Time::Local;
+#use Data::Dumper;
+
+#false laziness w/huawei_softx3000.pm
+%TZ = (
+  '+0000' => 'XXX-0',
+  '+0100' => 'XXX-1',
+  '+0200' => 'XXX-2',
+  '+0300' => 'XXX-3',
+  '+0400' => 'XXX-4',
+  '+0500' => 'XXX-5',
+  '+0600' => 'XXX-6',
+  '+0700' => 'XXX-7',
+  '+0800' => 'XXX-8',
+  '+0900' => 'XXX-9',
+  '+1000' => 'XXX-10',
+  '+1100' => 'XXX-11',
+  '+1200' => 'XXX-12',
+  '-0000' => 'XXX+0',
+  '-0100' => 'XXX+1',
+  '-0200' => 'XXX+2',
+  '-0300' => 'XXX+3',
+  '-0400' => 'XXX+4',
+  '-0500' => 'XXX+5',
+  '-0600' => 'XXX+6',
+  '-0700' => 'XXX+7',
+  '-0800' => 'XXX+8',
+  '-0900' => 'XXX+9',
+  '-1000' => 'XXX+10',
+  '-1100' => 'XXX+11',
+  '-1200' => 'XXX+12',
+);
+
+%info = (
+  'name'          => 'GSM TAP3 release 12',
+  'weight'        => 50,
+  'type'          => 'asn.1',
+  'import_fields' => [],
+  'asn_format'    => {
+    'spec'     => _asn_spec(),
+    'macro'    => 'TransferBatch', #XXX & skip the Notification ones?
+    'header_buffer' => sub {
+      my $TransferBatch = shift;
+
+      my $networkInfo = $TransferBatch->{networkInfo};
+
+      my $recEntityInfo = $networkInfo->{recEntityInfo};
+      my %recEntity = map { $_->{recEntityCode} => $_->{recEntityId} } @$recEntityInfo;
+
+      my $utcTimeOffsetInfo = $networkInfo->{utcTimeOffsetInfo};
+      my %utcTimeOffset = map { $_->{utcTimeOffsetCode} => $_->{utcTimeOffset} } @$utcTimeOffsetInfo;
+
+      { recEntity        => \%recEntity,
+        utcTimeOffset    => \%utcTimeOffset,
+        tapDecimalPlaces => $TransferBatch->{accountingInfo}{tapDecimalPlaces},
+      };
+    },
+    'arrayref' => sub { shift->{'callEventDetails'}; },
+    'map'      => {
+      'startdate'          => sub { my($row, $buffer) = @_;
+                                    my $callinfo = $row->{mobileOriginatedCall}{basicCallInformation};
+                                    my $timestamp = $callinfo->{callEventStartTimeStamp};
+
+                                    my $localTimeStamp = $timestamp->{localTimeStamp};
+                                    $localTimeStamp =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/
+                                      or die "unparsable timestamp: $localTimeStamp\n"; #. Dumper($callinfo);
+                                    my($year, $mon, $day, $hour, $min, $sec) = ($1, $2, $3, $4, $5, $6);
+
+                                    my $utcTimeOffsetCode = $timestamp->{utcTimeOffsetCode};
+                                    my $utcTimeOffset = $buffer->{utcTimeOffset}{ $utcTimeOffsetCode };
+                                    local($ENV{TZ}) = $TZ{ $utcTimeOffset };
+
+                                    timelocal($sec, $min, $hour, $day, $mon-1, $year);
+                                  },
+      'duration'           => sub { shift->{mobileOriginatedCall}{basicCallInformation}{totalCallEventDuration} },
+      'billsec'            => sub { shift->{mobileOriginatedCall}{basicCallInformation}{totalCallEventDuration} }, #same..
+      'src'                => sub { shift->{mobileOriginatedCall}{basicCallInformation}{chargeableSubscriber}{simChargeableSubscriber}{msisdn} },
+      'charged_party_imsi' => sub { shift->{mobileOriginatedCall}{basicCallInformation}{chargeableSubscriber}{simChargeableSubscriber}{imsi} },
+      'dst'                => sub { shift->{mobileOriginatedCall}{basicCallInformation}{destination}{calledNumber} }, #dialledDigits?
+      'carrierid'          => sub { my( $row, $buffer ) = @_;
+                                    my $recEntityCode = $row->{mobileOriginatedCall}{locationInformation}{networkLocation}{recEntityCode};
+                                    $buffer->{recEntity}{ $recEntityCode };
+                                  },
+      'userfield'          => sub { shift->{mobileOriginatedCall}{operatorSpecInformation}[0] },
+      'servicecode'        => sub { shift->{mobileOriginatedCall}{basicServiceUsedList}[0]{basicService}{serviceCode}{teleServiceCode} },
+      'upstream_price'     => sub { my($row, $buffer) = @_;
+                                    sprintf('%.'.$buffer->{tapDecimalPlaces}.'f',
+                                      $row->{mobileOriginatedCall}{basicServiceUsedList}[0]{chargeInformationList}[0]{chargeDetailList}[0]{charge}
+                                      / ( 10 ** $buffer->{tapDecimalPlaces} )
+                                    )
+                                  },
+      'calltypenum'        => sub { shift->{mobileOriginatedCall}{basicServiceUsedList}[0]{chargeInformationList}[0]{callTypeGroup}{callTypelevel1} },
+      'quantity'           => sub { shift->{mobileOriginatedCall}{basicServiceUsedList}[0]{chargeInformationList}[0]{chargedUnits} },
+      'quantity_able'      => sub { shift->{mobileOriginatedCall}{basicServiceUsedList}[0]{chargeInformationList}[0]{chargeableUnits} },
+    },
+  },
+);
+
+#accepts qsearch parameters as a hash or list of name/value pairs, but not
+#old-style qsearch('cdr', { field=>'value' })
+
+use Date::Format;
+use FS::Conf;
+sub tap3_12_export {
+  my %qsearch = ();
+  if ( ref($_[0]) eq 'HASH' ) {
+    %qsearch = %{ $_[0] };
+  } else {
+    %qsearch = @_;
+  }
+
+  #if these get huge we might need to get a count and do a paged search
+  my @cdrs = qsearch({ 'table'=>'cdr', %qsearch, 'order_by'=>'calldate ASC' });
+
+  my $conf = new FS::Conf;
+
+  eval "use Convert::ASN1";
+  die $@ if $@;
+
+  my $asn = Convert::ASN1->new;
+  $asn->prepare( _asn_spec() ) or die $asn->error;
+
+  my $TransferBatch = $asn->find('TransferBatch') or die $asn->error;
+
+  my %hash = _TransferBatch(); #static information etc.
+
+  my $now = time;
+  my $utcTimeOffset = time2str('%z', $now);
+
+  ###
+  # accountingInfo
+  ###
+
+  #mandatory
+  $hash{localCurrency} = $conf->config('currency') || 'USD';
+
+  ###
+  # batchControlInfo
+  ###
+
+  #optional
+  $hash{batchControlInfo}->{fileCreationTimeStamp}   = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $now),
+                                                         'utcTimeOffset'  => $utcTimeOffset,
+                                                       };
+
+  #The timestamp used to select calls for transfer.  All call records available prior to the timestamp are transferred.
+  # This gives an indication to the HPMN as to how ‘up-to-date’ the information is.
+  $hash{batchControlInfo}->{transferCutOffTimeStamp} = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $cdrs[-1]->calldate_unix ),
+                                                         'utcTimeOffset'  => $utcTimeOffset,
+                                                       };
+
+  #The date and time at which the file was made available to the Recipient PMN.
+  # Physically this will normally be the timestamp when the file transfer
+  # commenced to the Recipient PMN, i.e. start of push, however on some systems
+  # this will be the timestamp when the file was made available to be pulled.
+  $hash{batchControlInfo}->{fileAvailableTimeStamp}  = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $now),
+                                                          'utcTimeOffset'  => $utcTimeOffset,
+                                                        };
+
+  # A unique identifier used to determine the network which is the Sender of the data.
+  # The full list of codes in use is given in TADIG PRD TD.13: PMN Naming Conventions.
+  $hash{batchControlInfo}->{sender} = $conf->config('cdr-gsm_tap3-sender') || 'ZZZZZ'; #reserved: Y*, ZO-ZZ
+
+  #XXX customer or agent field of some sort
+  # A unique identifier used to determine which network the data is being sent to,
+  #  i.e. the Recipient.
+  # Derivation: GSM Association PRD TD.13: PMN Naming Conventions.
+  $hash{batchControlInfo}->{recipient} = 'GNQHT';
+
+  #XXX
+  #A unique reference which identifies each TAP Data Interchange sent by one PMN to another, specific, PMN.
+  # The sequence commences at 1 and is incremented by one for each subsequent TAP Data Interchange sent by the Sender PMN to a particular Recipient PMN.
+  # Separate sequence numbering must be used for Test Data and Chargeable Data.  Having reached the maximum value (99999) the number must recycle to 1.
+  $hash{batchControlInfo}->{fileSequenceNumber} = '00178';
+
+  ###
+  # networkInfo
+  ###
+
+  $hash{networkInfo}->{utcTimeOffsetInfo}[0]{utcTimeOffset} = $utcTimeOffset;
+
+  #XXX recording entity IDs, referenced by recEntityCode
+  #$hash->{networkInfo}->{recEntityInfo}[0]{recEntityId} = '340010100';
+  #$hash->{networkInfo}->{recEntityInfo}[1]{recEntityId} = '240556000000';
+
+  ###
+  # auditControlInfo
+  ###
+
+  #mandatory
+  $hash{auditControlInfo}->{callEventDetailsCount} = scalar(@cdrs);
+
+  #these two are optional
+  $hash{auditControlInfo}->{earliestCallTimeStamp} = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $cdrs[0]->calldate_unix),
+                                                       'utcTimeOffset'  => $utcTimeOffset,
+                                                     };
+  $hash{auditControlInfo}->{latestCallTimeStamp}   = { 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $cdrs[-1]->calldate_unix),
+                                                       'utcTimeOffset'  => $utcTimeOffset,
+                                                     };
+
+  #mandatory
+  my $totalCharge = 0;
+  $totalCharge += $_->rated_price foreach @cdrs;
+  $hash{totalCharge} = sprintf('%.5f', $totalCharge);
+
+  ###
+  # callEventDetails
+  ###
+
+  $hash{callEventDetails} = [ map tap3_12_export_cdr($_), @cdrs ];
+
+  ###
+
+  $TransferBatch->encode( \%hash );
+
+}
+
+sub _TransferBatch {
+
+  #accounting related information
+  'accountingInfo'   => {
+                          #mandatory
+                          #'localCurrency'          => 'USD',
+                          'tapDecimalPlaces'       => 5,
+                          'currencyConversionInfo' => [
+                                                        {
+                                                          'numberOfDecimalPlaces' => 5,
+                                                          'exchangeRate'          => 152549, #XXX ??? "exchange rate +VAT" ?
+                                                          'exchangeRateCode'      => 1
+                                                        }
+                                                      ],
+                          #optional: may conditionally include taxation and discounting tables, and, optionally, TAP currency
+                        },
+
+  'batchControlInfo' => {
+                          #mandatory
+                          'specificationVersionNumber' => 3,
+                          'releaseVersionNumber'       => 12,
+
+                          #'sender' => 'MDGTM',
+                          #'recipient' => 'GNQHT',
+                          #'fileSequenceNumber' => '00178',
+
+                          #'transferCutOffTimeStamp' => {
+                          #                               'localTimeStamp' => '20121230050222',
+                          #                               'utcTimeOffset' => '+0300'
+                          #                             },
+                          #'fileAvailableTimeStamp' => {
+                          #                              'localTimeStamp' => '20121230035052',
+                          #                              'utcTimeOffset' => '+0100'
+                          #                            }
+
+                          #optional
+                          #'fileCreationTimeStamp' => {
+                          #                             'localTimeStamp' => '20121230050222',
+                          #                             'utcTimeOffset' => '+0300'
+                          #                           },
+
+                          #optional: file type indicator which will only be present where the file represents test data
+                          #optional: RAP File Sequence Number (used where the batch has previously been returned with a fatal error and is now being resubmitted) (not fileSequenceNumber?)
+
+                          #optional: beyond the scope of TAP and has been bilaterally agreed
+                          #'operatorSpecInformation' => [
+                          #                               '', # '|File proc MTH LUXMA: 1285348027|' Operator Specific Information
+                          #                                   # probably just leave out
+                          #                             ],
+     
+
+                        },
+
+  #Network Information is a group of related information which pertains to the Sender PMN
+  'networkInfo'      => {
+                          #must be present where Recording Entity Codes are present within the TAP file
+                          'recEntityInfo'     => [
+                                                   {
+                                                     'recEntityCode' => 1,
+                                                     'recEntityType' => 1, #MSC
+                                                     #'recEntityId'   => '340010100',
+                                                   },
+                                                   {
+                                                     'recEntityCode' => 2,
+                                                     'recEntityType' => 2, #SMSC
+                                                     #'recEntityId'   => '240556000000',
+                                                   },
+                                                 ],
+                          #mandatory
+                          'utcTimeOffsetInfo' => [
+                                                   {
+                                                     'utcTimeOffsetCode' => 1,
+                                                     #'utcTimeOffset'     => '+0300',
+                                                   }
+                                                 ]
+                        },
+
+  #identifies the end of the Transfer Batch
+  'auditControlInfo' => {
+                          #mandatory
+                          #'callEventDetailsCount' => 4,
+                          'totalTaxValue'         => 0,
+                          'totalDiscountValue'    => 0,
+                          #'totalCharge'           => 50474,
+
+                          #these two are optional
+                          #'earliestCallTimeStamp' => {
+                          #                             'localTimeStamp' => '20121229102501',
+                          #                             'utcTimeOffset' => '+0300'
+                          #                           },
+                          #'latestCallTimeStamp'   => {
+                          #                             'localTimeStamp' => '20121229102807',
+                          #                             'utcTimeOffset' => '+0300'
+                          #                           }
+                          #optional: beyond the scope of TAP and has been bilaterally agreed
+                          #'operatorSpecInformation' => [
+                          #                               '',
+                          #                             ],
+                        },
+}
+
+sub tap3_12_export_cdr {
+  my $self = shift;
+
+  #one of Mobile Originated Call, Mobile Terminated Call, Mobile Session, Messaging Event, Supplementary Service Event, Service Centre Usage, GPRS Call, Content Transaction or Location Service
+  # Each occurrence must have no more than one of these present
+
+  { #either tele or bearer service usage originated by the mobile subscription (others?)
+    'mobileOriginatedCall' => {
+
+      #identifies the Network Location, which includes the MSC responsible for handling
+      # the call and, where appropriate, the Geographical Location of the mobile
+      'locationInformation' => {
+                                 'networkLocation' => {
+                                                        'recEntityCode' => $self->carrierid, #XXX Recording Entity (per 2.5, from "Reference Tables")
+                                                      }
+                               },
+
+      #Operator Specific Information: beyond the scope of TAP and has been bilaterally agreed
+      'operatorSpecInformation' => [
+                                     $self->userfield, ##'|Seq: 178 Loc: 1|'
+                                   ],
+
+      #The type of service used together with all related charging information
+      'basicServiceUsedList' => [
+                                  {
+                                    #identifies the actual Basic Service used
+                                    'basicService' => {
+                                                        #one of Teleservice Code or Bearer Service Code as determined by the service type used
+                                                        'serviceCode' => {
+                                                                           #XXX
+                                                                           #00 All teleservices
+                                                                           #10 All Speech transmission services
+                                                                           #11 Telephony
+                                                                           #12 Emergency calls
+                                                                           #20 All SMS Services
+                                                                           #21 Short Message MT/PP
+                                                                           #22 Short Message MO/PP
+                                                                           #60 All Fax Services
+                                                                           #61 Facsimile Group 3 & alternative speech
+                                                                           #62 Automatic Facsimile Group 3
+                                                                           #63 Automatic Facsimile Group 4
+                                                                           #70 All data teleservices (compound)
+                                                                           #80 All teleservices except SMS (compound)
+                                                                           #90 All voice group call services
+                                                                           #91 Voice group call
+                                                                           #92 Voice broadcast call
+                                                                           'teleServiceCode' => $self->servicecode, #'11'
+
+                                                                           #Bearer Service Code
+                                                                           # Must be present within group Service Code where the type of service used
+                                                                           #  was a bearer service. Must not be present when the type of service used
+                                                                           #  was a tele service and, therefore, Teleservice Code is present.
+                                                                           # Group Bearer Codes, identifiable by the description ‘All’, should only
+                                                                           #  be used where details of the specific services affected are not
+                                                                           #  available from the network.
+                                                                           #00 All Bearer Services
+                                                                           #20 All Data Circuit Asynchronous Services
+                                                                           #21 Duplex Asynch. 300bps data circuit
+                                                                           #22 Duplex Asynch. 1200bps data circuit
+                                                                           #23 Duplex Asynch. 1200/75bps data circuit
+                                                                           #24 Duplex Asynch. 2400bps data circuit
+                                                                           #25 Duplex Asynch. 4800bps data circuit
+                                                                           #26 Duplex Asynch. 9600bps data circuit
+                                                                           #27 General Data Circuit Asynchronous Service
+                                                                           #30 All Data Circuit Synchronous Services
+                                                                           #32 Duplex Synch. 1200bps data circuit
+                                                                           #34 Duplex Synch. 2400bps data circuit
+                                                                           #35 Duplex Synch. 4800bps data circuit
+                                                                           #36 Duplex Synch. 9600bps data circuit
+                                                                           #37 General Data Circuit Synchronous Service
+                                                                           #40 All Dedicated PAD Access Services
+                                                                           #41 Duplex Asynch. 300bps PAD access
+                                                                           #42 Duplex Asynch. 1200bps PAD access
+                                                                           #43 Duplex Asynch. 1200/75bps PAD access
+                                                                           #44 Duplex Asynch. 2400bps PAD access
+                                                                           #45 Duplex Asynch. 4800bps PAD access
+                                                                           #46 Duplex Asynch. 9600bps PAD access
+                                                                           #47 General PAD Access Service
+                                                                           #50 All Dedicated Packet Access Services
+                                                                           #54 Duplex Synch. 2400bps PAD access
+                                                                           #55 Duplex Synch. 4800bps PAD access
+                                                                           #56 Duplex Synch. 9600bps PAD access
+                                                                           #57 General Packet Access Service
+                                                                           #60 All Alternat Speech/Asynchronous Services
+                                                                           #70 All Alternate Speech/Synchronous Services
+                                                                           #80 All Speech followed by Data Asynchronous Services
+                                                                           #90 All Speech followed by Data Synchronous Services
+                                                                           #A0 All Data Circuit Asynchronous Services (compound)
+                                                                           #B0 All Data Circuit Synchronous Services (compound)
+                                                                           #C0 All Asynchronous Services (compound)
+                                                                         }
+                                                        #conditionally also contain the following for UMTS: Transparency Indicator, Fixed Network User
+                                                        # Rate, User Protocol Indicator, Guaranteed Bit Rate and Maximum Bit Rate
+                                                      },
+
+                                    #Charge information is provided for all chargeable elements except within Messaging Event and Mobile Session call events
+                                    # must contain Charged Item and at least one occurrence of Charge Detail
+                                    'chargeInformationList' => [
+                                                                 {
+                                                                   #XXX
+                                                                   #mandatory
+                                                                   # the charging principle applied and the unitisation of Chargeable Units.  It
+                                                                   #  is not intended to identify the service used.
+                                                                   #A: Call set up attempt
+                                                                   #C: Content
+                                                                   #D: Duration based charge
+                                                                   #E: Event based charge
+                                                                   #F: Fixed (one-off) charge
+                                                                   #L: Calendar (for example daily usage charge)
+                                                                   #V: Volume (outgoing) based charge
+                                                                   #W: Volume (incoming) based charge
+                                                                   #X: Volume (total volume) based charge
+                                                                   #(?? fields to be used as a basis for the calculation of the correct Charge
+                                                                   #  A: Chargeable Units (if present)
+                                                                   #  D,V,W,X: Chargeable Units
+                                                                   #  C: Depends on the content
+                                                                   #  E: Not Applicable
+                                                                   #  F: Not Applicable
+                                                                   #  L: Call Event Start Timestamp)
+                                                                   'chargedItem' => 'D',
+
+                                                                   # the IOT used by the VPMN to price the call
+                                                                   'callTypeGroup' => {
+
+                                                                                        #The highest category call type in respect of the destination of the call
+                                                                                        #0: Unknown/Not Applicable
+                                                                                        #1: National
+                                                                                        #2: International
+                                                                                        #10: HGGSN/HP-GW
+                                                                                        #11: VGGSN/VP-GW
+                                                                                        #12: Other GGSN/Other P-GW
+                                                                                        #100: WLAN
+                                                                                        'callTypeLevel1' => $self->calltypenum,
+
+                                                                                        #the sub category of Call Type Level 1
+                                                                                        #0: Unknown/Not Applicable
+                                                                                        #1: Mobile
+                                                                                        #2: PSTN
+                                                                                        #3: Non Geographic
+                                                                                        #4: Premium Rate
+                                                                                        #5: Satellite destination
+                                                                                        #6: Forwarded call
+                                                                                        #7: Non forwarded call
+                                                                                        #10: Broadband
+                                                                                        #11: Narrowband
+                                                                                        #12: Conversational
+                                                                                        #13: Streaming
+                                                                                        #14: Interactive
+                                                                                        #15: Background
+                                                                                        'callTypeLevel2' => 0,
+
+                                                                                        #the sub category of Call Type Level 2
+                                                                                        'callTypeLevel3' => 0,
+                                                                                      },
+
+                                                                   #mandatory, at least one occurence must be present
+                                                                   #A repeating group detailing the Charge and/or charge element
+                                                                   # Note that, where a Charge has been levied, even where that Charge is zero,
+                                                                   #  there must be one occurance, and only one, with a Charge Type of '00'
+                                                                   'chargeDetailList' => [
+                                                                                           {
+                                                                                             #mandatory
+                                                                                             # after discounts have been deducted but before any tax is added
+                                                                                             'charge'          => $self->rated_price * 100000, #XXX numberOfDecimalPlaces 
+
+                                                                                             #mandatory
+                                                                                             # the type of charge represented
+                                                                                             #00: Total charge for Charge Information (the invoiceable value)
+                                                                                             #01: Airtime charge
+                                                                                             #02: reserved
+                                                                                             #03: Toll charge
+                                                                                             #04: Directory assistance
+                                                                                             #05–20: reserved
+                                                                                             #21: VPMN surcharge
+                                                                                             #50: Total charge for Charge Information according to the published IOT
+                                                                                             #  Note that the use of value 50 is only for use by bilateral agreement, use without
+                                                                                             #   bilateral agreement can be treated as per reserved values, that is ‘out of range’
+                                                                                             #69–99: reserved
+                                                                                             'chargeType'      => '00',
+
+                                                                                             #conditional
+                                                                                             # the number of units which are chargeable within the Charge Detail, this may not
+                                                                                             # correspond to the number of rounded units charged.
+                                                                                             # The item Charged Item defines what the units represent.
+                                                                                             'chargeableUnits' => $self->quantity_able,
+
+                                                                                             #optional
+                                                                                             # the rounded number of units which are actually charged for
+                                                                                             'chargedUnits'    => $self->quantity,
+                                                                                           }
+                                                                                         ],
+                                                                   'exchangeRateCode' => 1, #from header
+                                                                 }
+                                                               ]
+                                  }
+                                ],
+
+      #MO Basic Call Information provides the basic detail of who made the call and where to in respect of mobile originated traffic.
+      'basicCallInformation' => {
+                                  #mandatory
+                                  # the identification of the chargeable subscriber.
+                                  #  The group must contain either the IMSI or the MIN of the Chargeable Subscriber, but not both.
+                                  'chargeableSubscriber' => {
+                                                              'simChargeableSubscriber' => {
+                                                                                             'msisdn' => $self->charged_party, #src
+                                                                                             'imsi'   => $self->charged_party_imsi,
+                                                                                           }
+                                                            },
+                                  # the start of the call event
+                                  'callEventStartTimeStamp' => {
+                                                                 'localTimeStamp' => time2str('%Y%m%d%H%M%S', $self->startdate),
+                                                                 'utcTimeOffsetCode' => 1
+                                                               },
+
+                                  # the actual total duration of a call event as a number of seconds
+                                  'totalCallEventDuration' => $self->duration,
+
+                                  #conditional
+                                  # the number dialled by the subscriber (Called Number)
+                                  #  or the SMSC Address in case of SMS usage or in cases involving supplementary services
+                                  #   such as call forwarding or transfer etc., the number to which the call is routed
+                                  'destination' => {
+                                                     #the international representation of the destination
+                                                     'calledNumber' => $self->dst,
+
+                                                     #the actual digits as dialled by the subscriber, i.e. unmodified, in establishing a call
+                                                     # This will contain ‘+’ and ‘#’ where appropriate.
+                                                     #'dialledDigits' => '322221350'
+                                                   },
+                                }
+    }
+  };
+
+}
+
+sub _asn_spec {
+  <<'END';
+--
+--
+-- The following ASN.1 specification defines the abstract syntax for 
+--
+--        Data Record Format Version 03 
+--                           Release 12
+--
+-- The specification is structured as follows:
+--   (1) structure of the Tap batch
+--   (2) definition of the individual Tap ‘records’ 
+--   (3) Tap data items and groups of data items used within (2)
+--   (4) Common, non-Tap data types
+--   (5) Tap data items for content charging
+--
+-- It is mainly a translation from the logical structure
+-- diagrams. Where appropriate, names used within the 
+-- logical structure diagrams have been shortened.
+-- For repeating data items the name as used within the logical
+-- structure have been extended by adding ‘list’ or ‘table’
+-- (in some instances).
+--
+
+
+-- TAP-0312  DEFINITIONS IMPLICIT TAGS  ::= 
+
+-- BEGIN 
+
+--
+-- Structure of a Tap batch 
+--
+
+DataInterChange ::= CHOICE 
+{
+    transferBatch TransferBatch, 
+    notification  Notification,
+...
+}
+
+-- Batch Control Information must always, both logically and physically,
+-- be the first group/item within Transfer Batch – this ensures that the
+-- TAP release version can be readily identified.  Any new groups/items
+-- required may be inserted at any point after Batch Control Information
+
+TransferBatch ::= [APPLICATION 1] SEQUENCE
+{
+    batchControlInfo       BatchControlInfo            OPTIONAL, -- *m.m.
+    accountingInfo         AccountingInfo              OPTIONAL,
+    networkInfo            NetworkInfo                 OPTIONAL, -- *m.m.
+    messageDescriptionInfo MessageDescriptionInfoList  OPTIONAL,
+    callEventDetails       CallEventDetailList         OPTIONAL, -- *m.m.
+    auditControlInfo       AuditControlInfo            OPTIONAL, -- *m.m.
+...
+}
+
+Notification ::= [APPLICATION 2] SEQUENCE
+{
+    sender                     Sender                     OPTIONAL, -- *m.m.
+    recipient                   Recipient                  OPTIONAL, -- *m.m.
+    fileSequenceNumber          FileSequenceNumber         OPTIONAL, -- *m.m.
+    rapFileSequenceNumber       RapFileSequenceNumber      OPTIONAL,
+    fileCreationTimeStamp       FileCreationTimeStamp      OPTIONAL,
+    fileAvailableTimeStamp      FileAvailableTimeStamp     OPTIONAL, -- *m.m.
+    transferCutOffTimeStamp     TransferCutOffTimeStamp    OPTIONAL, -- *m.m.
+    specificationVersionNumber SpecificationVersionNumber OPTIONAL, -- *m.m.
+    releaseVersionNumber        ReleaseVersionNumber       OPTIONAL, -- *m.m.
+    fileTypeIndicator           FileTypeIndicator          OPTIONAL,
+    operatorSpecInformation     OperatorSpecInfoList       OPTIONAL,
+...
+}
+
+CallEventDetailList ::=  [APPLICATION 3] SEQUENCE OF CallEventDetail
+
+CallEventDetail ::= CHOICE
+{
+    mobileOriginatedCall   MobileOriginatedCall,
+    mobileTerminatedCall   MobileTerminatedCall,
+    supplServiceEvent      SupplServiceEvent,
+    serviceCentreUsage     ServiceCentreUsage,
+    gprsCall               GprsCall,
+    contentTransaction     ContentTransaction,
+    locationService        LocationService,
+    messagingEvent         MessagingEvent,
+    mobileSession          MobileSession,
+...
+}
+
+--
+-- Structure of the individual Tap records
+--
+
+BatchControlInfo ::= [APPLICATION 4] SEQUENCE
+{
+    sender                      Sender                         OPTIONAL, -- *m.m.
+    recipient                   Recipient                              OPTIONAL, -- *m.m.
+    fileSequenceNumber          FileSequenceNumber             OPTIONAL, -- *m.m.
+    fileCreationTimeStamp       FileCreationTimeStamp          OPTIONAL,
+    transferCutOffTimeStamp     TransferCutOffTimeStamp        OPTIONAL, -- *m.m.
+    fileAvailableTimeStamp      FileAvailableTimeStamp         OPTIONAL, -- *m.m.
+    specificationVersionNumber SpecificationVersionNumber      OPTIONAL, -- *m.m.
+    releaseVersionNumber        ReleaseVersionNumber           OPTIONAL, -- *m.m.
+    fileTypeIndicator           FileTypeIndicator              OPTIONAL,
+    rapFileSequenceNumber       RapFileSequenceNumber          OPTIONAL,
+    operatorSpecInformation     OperatorSpecInfoList           OPTIONAL,
+...
+}
+
+AccountingInfo ::= [APPLICATION 5] SEQUENCE
+{
+    taxation                  TaxationList           OPTIONAL,
+    discounting               DiscountingList        OPTIONAL,
+    localCurrency             LocalCurrency          OPTIONAL, -- *m.m.
+    tapCurrency               TapCurrency            OPTIONAL,
+    currencyConversionInfo    CurrencyConversionList OPTIONAL,
+    tapDecimalPlaces          TapDecimalPlaces       OPTIONAL, -- *m.m.
+...
+}
+
+NetworkInfo ::= [APPLICATION 6] SEQUENCE
+{
+    utcTimeOffsetInfo         UtcTimeOffsetInfoList OPTIONAL, -- *m.m.
+    recEntityInfo             RecEntityInfoList     OPTIONAL,
+...
+}
+
+MessageDescriptionInfoList ::= [APPLICATION 8] SEQUENCE OF MessageDescriptionInformation
+
+MobileOriginatedCall ::= [APPLICATION 9] SEQUENCE
+{
+    basicCallInformation    MoBasicCallInformation    OPTIONAL, -- *m.m.
+    locationInformation     LocationInformation       OPTIONAL, -- *m.m.
+    equipmentIdentifier     ImeiOrEsn                 OPTIONAL,
+    basicServiceUsedList    BasicServiceUsedList      OPTIONAL, -- *m.m.
+    supplServiceCode        SupplServiceCode          OPTIONAL,
+    thirdPartyInformation   ThirdPartyInformation     OPTIONAL,
+    camelServiceUsed        CamelServiceUsed          OPTIONAL,
+    operatorSpecInformation OperatorSpecInfoList      OPTIONAL,
+...
+}    
+
+MobileTerminatedCall ::= [APPLICATION 10] SEQUENCE
+{
+    basicCallInformation    MtBasicCallInformation    OPTIONAL, -- *m.m.
+    locationInformation     LocationInformation       OPTIONAL, -- *m.m.
+    equipmentIdentifier     ImeiOrEsn                 OPTIONAL,
+    basicServiceUsedList    BasicServiceUsedList      OPTIONAL, -- *m.m.
+    camelServiceUsed        CamelServiceUsed          OPTIONAL,
+    operatorSpecInformation OperatorSpecInfoList      OPTIONAL,
+...
+}    
+
+
+SupplServiceEvent ::= [APPLICATION 11] SEQUENCE
+{
+    chargeableSubscriber      ChargeableSubscriber    OPTIONAL, -- *m.m.
+    rapFileSequenceNumber     RapFileSequenceNumber   OPTIONAL,
+    locationInformation       LocationInformation     OPTIONAL, -- *m.m.
+    equipmentIdentifier       ImeiOrEsn               OPTIONAL,
+    supplServiceUsed          SupplServiceUsed        OPTIONAL, -- *m.m.
+    operatorSpecInformation   OperatorSpecInfoList    OPTIONAL,
+...
+}
+
+
+ServiceCentreUsage ::= [APPLICATION 12] SEQUENCE
+{
+    basicInformation          ScuBasicInformation     OPTIONAL, -- *m.m.
+    rapFileSequenceNumber     RapFileSequenceNumber   OPTIONAL,
+    servingNetwork            ServingNetwork          OPTIONAL,
+    recEntityCode             RecEntityCode           OPTIONAL, -- *m.m.
+    chargeInformation         ChargeInformation       OPTIONAL, -- *m.m.
+    scuChargeType             ScuChargeType           OPTIONAL, -- *m.m.
+    scuTimeStamps             ScuTimeStamps           OPTIONAL, -- *m.m.
+    operatorSpecInformation   OperatorSpecInfoList    OPTIONAL,
+...
+}
+
+GprsCall ::= [APPLICATION 14] SEQUENCE
+{
+    gprsBasicCallInformation  GprsBasicCallInformation  OPTIONAL, -- *m.m.
+    gprsLocationInformation   GprsLocationInformation   OPTIONAL, -- *m.m.
+    equipmentIdentifier       ImeiOrEsn                 OPTIONAL,
+    gprsServiceUsed           GprsServiceUsed           OPTIONAL, -- *m.m.
+    camelServiceUsed          CamelServiceUsed          OPTIONAL,
+    operatorSpecInformation   OperatorSpecInfoList      OPTIONAL,
+...
+}
+
+ContentTransaction ::= [APPLICATION 17] SEQUENCE
+{
+ contentTransactionBasicInfo ContentTransactionBasicInfo OPTIONAL, -- *m.m.
+ chargedPartyInformation     ChargedPartyInformation     OPTIONAL, -- *m.m.
+ servingPartiesInformation   ServingPartiesInformation   OPTIONAL, -- *m.m.
+ contentServiceUsed          ContentServiceUsedList      OPTIONAL, -- *m.m.
+ operatorSpecInformation     OperatorSpecInfoList        OPTIONAL,
+...
+}
+
+LocationService ::= [APPLICATION 297] SEQUENCE
+{
+    rapFileSequenceNumber        RapFileSequenceNumber       OPTIONAL,
+    recEntityCode                        RecEntityCode               OPTIONAL, -- *m.m.
+    callReference                        CallReference               OPTIONAL,
+    trackingCustomerInformation TrackingCustomerInformation OPTIONAL,
+    lCSSPInformation             LCSSPInformation            OPTIONAL,
+    trackedCustomerInformation  TrackedCustomerInformation  OPTIONAL,
+    locationServiceUsage         LocationServiceUsage        OPTIONAL, -- *m.m.
+    operatorSpecInformation      OperatorSpecInfoList        OPTIONAL,
+...
+}
+
+MessagingEvent ::= [APPLICATION 433] SEQUENCE
+{
+    messagingEventService      MessagingEventService     OPTIONAL, -- *m.m.
+    chargedParty              ChargedParty              OPTIONAL, -- *m.m.
+    rapFileSequenceNumber      RapFileSequenceNumber     OPTIONAL,
+    simToolkitIndicator                SimToolkitIndicator       OPTIONAL,
+    geographicalLocation       GeographicalLocation      OPTIONAL,
+    eventReference            EventReference             OPTIONAL, -- *m.m.
+
+    recEntityCodeList                  RecEntityCodeList         OPTIONAL, -- *m.m.  
+    networkElementList         NetworkElementList        OPTIONAL,
+    locationArea              LocationArea               OPTIONAL,
+    cellId                     CellId                            OPTIONAL,    
+    serviceStartTimestamp      ServiceStartTimestamp     OPTIONAL, -- *m.m.
+    nonChargedParty            NonChargedParty           OPTIONAL,
+    exchangeRateCode           ExchangeRateCode                  OPTIONAL,
+    callTypeGroup                      CallTypeGroup             OPTIONAL, -- *m.m.
+    charge                             Charge                    OPTIONAL, -- *m.m.
+    taxInformationList         TaxInformationList        OPTIONAL,
+    operatorSpecInformation   OperatorSpecInfoList      OPTIONAL,
+...
+}
+
+MobileSession ::= [APPLICATION 434] SEQUENCE
+{
+    mobileSessionService       MobileSessionService      OPTIONAL, -- *m.m.
+    chargedParty              ChargedParty              OPTIONAL, -- *m.m.
+    rapFileSequenceNumber      RapFileSequenceNumber     OPTIONAL,
+    simToolkitIndicator                SimToolkitIndicator       OPTIONAL,
+    geographicalLocation       GeographicalLocation      OPTIONAL,
+    locationArea              LocationArea               OPTIONAL,
+    cellId                     CellId                            OPTIONAL,
+    eventReference            EventReference             OPTIONAL, -- *m.m.
+
+    recEntityCodeList                  RecEntityCodeList         OPTIONAL, -- *m.m.
+    serviceStartTimestamp      ServiceStartTimestamp     OPTIONAL, -- *m.m.
+    causeForTerm              CauseForTerm             OPTIONAL,
+    totalCallEventDuration     TotalCallEventDuration    OPTIONAL, -- *m.m.
+    nonChargedParty            NonChargedParty           OPTIONAL,
+    sessionChargeInfoList     SessionChargeInfoList     OPTIONAL, -- *m.m.
+    operatorSpecInformation   OperatorSpecInfoList      OPTIONAL,
+...
+}
+
+AuditControlInfo ::= [APPLICATION 15] SEQUENCE
+{
+    earliestCallTimeStamp        EarliestCallTimeStamp       OPTIONAL,
+    latestCallTimeStamp          LatestCallTimeStamp         OPTIONAL,
+    totalCharge                  TotalCharge                 OPTIONAL, -- *m.m.
+    totalChargeRefund            TotalChargeRefund           OPTIONAL,
+    totalTaxRefund               TotalTaxRefund              OPTIONAL,
+    totalTaxValue                TotalTaxValue               OPTIONAL, -- *m.m.
+    totalDiscountValue           TotalDiscountValue          OPTIONAL, -- *m.m.
+    totalDiscountRefund                  TotalDiscountRefund         OPTIONAL,
+    totalAdvisedChargeValueList TotalAdvisedChargeValueList OPTIONAL,
+    callEventDetailsCount        CallEventDetailsCount       OPTIONAL, -- *m.m.
+    operatorSpecInformation      OperatorSpecInfoList        OPTIONAL,
+...
+}
+
+
+-- 
+-- Tap data items and groups of data items
+--
+
+AccessPointNameNI ::= [APPLICATION 261] AsciiString --(SIZE(1..63))
+
+AccessPointNameOI ::= [APPLICATION 262] AsciiString --(SIZE(1..37))
+
+ActualDeliveryTimeStamp ::= [APPLICATION 302] DateTime
+
+AddressStringDigits ::= BCDString
+
+AdvisedCharge ::= [APPLICATION 349] Charge
+AdvisedChargeCurrency ::= [APPLICATION 348] Currency
+AdvisedChargeInformation ::= [APPLICATION 351] SEQUENCE
+{
+    paidIndicator         PaidIndicator         OPTIONAL,
+    paymentMethod         PaymentMethod         OPTIONAL,
+    advisedChargeCurrency AdvisedChargeCurrency OPTIONAL,
+    advisedCharge         AdvisedCharge         OPTIONAL, -- *m.m.
+    commission            Commission            OPTIONAL,
+...
+}
+AgeOfLocation ::= [APPLICATION 396] INTEGER
+
+BasicService ::= [APPLICATION 36] SEQUENCE
+{
+    serviceCode                 BasicServiceCode       OPTIONAL, -- *m.m.
+    transparencyIndicator       TransparencyIndicator  OPTIONAL,
+    fnur                        Fnur                   OPTIONAL,
+    userProtocolIndicator       UserProtocolIndicator  OPTIONAL,
+    guaranteedBitRate           GuaranteedBitRate      OPTIONAL,
+    maximumBitRate              MaximumBitRate         OPTIONAL,
+...
+}
+
+BasicServiceCode ::= [APPLICATION 426] CHOICE 
+{
+    teleServiceCode      TeleServiceCode,
+    bearerServiceCode    BearerServiceCode,
+...
+}
+
+BasicServiceCodeList ::= [APPLICATION 37] SEQUENCE OF BasicServiceCode
+
+BasicServiceUsed ::= [APPLICATION 39] SEQUENCE
+{
+    basicService                BasicService          OPTIONAL, -- *m.m.
+    chargingTimeStamp           ChargingTimeStamp     OPTIONAL,
+    chargeInformationList       ChargeInformationList OPTIONAL, -- *m.m.
+    hSCSDIndicator              HSCSDIndicator        OPTIONAL,
+...
+}
+
+BasicServiceUsedList ::= [APPLICATION 38] SEQUENCE OF BasicServiceUsed
+
+BearerServiceCode ::= [APPLICATION 40] HexString --(SIZE(2))
+
+EventReference ::= [APPLICATION 435]  AsciiString
+
+
+CalledNumber ::= [APPLICATION 407] AddressStringDigits
+
+CalledPlace ::= [APPLICATION 42] AsciiString
+
+CalledRegion ::= [APPLICATION 46] AsciiString
+
+CallEventDetailsCount ::= [APPLICATION 43] INTEGER 
+
+CallEventStartTimeStamp ::= [APPLICATION 44] DateTime
+
+CallingNumber ::= [APPLICATION 405] AddressStringDigits
+
+CallOriginator ::= [APPLICATION 41]  SEQUENCE
+{
+    callingNumber               CallingNumber          OPTIONAL,
+    clirIndicator               ClirIndicator         OPTIONAL,
+    sMSOriginator               SMSOriginator         OPTIONAL,
+...
+}
+
+CallReference ::= [APPLICATION 45] OCTET STRING --(SIZE(1..8))
+
+CallTypeGroup ::= [APPLICATION 258] SEQUENCE
+{
+    callTypeLevel1      CallTypeLevel1           OPTIONAL, -- *m.m.
+    callTypeLevel2      CallTypeLevel2           OPTIONAL, -- *m.m.
+    callTypeLevel3      CallTypeLevel3           OPTIONAL, -- *m.m.
+...
+}
+
+CallTypeLevel1 ::= [APPLICATION 259] INTEGER
+
+CallTypeLevel2 ::= [APPLICATION 255] INTEGER
+
+CallTypeLevel3 ::= [APPLICATION 256] INTEGER
+
+CamelDestinationNumber ::= [APPLICATION 404] AddressStringDigits
+
+CamelInvocationFee ::= [APPLICATION 422] AbsoluteAmount
+
+CamelServiceKey ::= [APPLICATION 55] INTEGER
+
+CamelServiceLevel ::= [APPLICATION 56] INTEGER
+
+CamelServiceUsed ::= [APPLICATION 57] SEQUENCE
+{
+    camelServiceLevel         CamelServiceLevel                OPTIONAL,
+    camelServiceKey           CamelServiceKey                  OPTIONAL, -- *m.m.
+    defaultCallHandling       DefaultCallHandlingIndicator     OPTIONAL,
+    exchangeRateCode          ExchangeRateCode                         OPTIONAL,
+    taxInformation            TaxInformationList               OPTIONAL,
+    discountInformation       DiscountInformation              OPTIONAL,
+    camelInvocationFee        CamelInvocationFee               OPTIONAL,
+    threeGcamelDestination    ThreeGcamelDestination           OPTIONAL,
+    cseInformation            CseInformation                   OPTIONAL,
+...
+}
+
+CauseForTerm ::= [APPLICATION 58] INTEGER
+
+CellId ::= [APPLICATION 59] INTEGER 
+
+Charge ::= [APPLICATION 62] AbsoluteAmount
+
+ChargeableSubscriber ::= [APPLICATION 427] CHOICE 
+{
+    simChargeableSubscriber SimChargeableSubscriber,
+    minChargeableSubscriber MinChargeableSubscriber,
+...
+}
+
+ChargeableUnits ::= [APPLICATION 65]  INTEGER
+
+ChargeDetail ::= [APPLICATION 63] SEQUENCE
+{
+    chargeType              ChargeType                         OPTIONAL, -- *m.m.
+    charge                  Charge                             OPTIONAL, -- *m.m.
+    chargeableUnits         ChargeableUnits                    OPTIONAL,
+    chargedUnits            ChargedUnits                       OPTIONAL,
+    chargeDetailTimeStamp   ChargeDetailTimeStamp      OPTIONAL,
+...
+}
+
+ChargeDetailList ::= [APPLICATION 64] SEQUENCE OF ChargeDetail
+
+ChargeDetailTimeStamp ::= [APPLICATION 410] ChargingTimeStamp
+
+ChargedItem ::= [APPLICATION 66]  AsciiString --(SIZE(1))
+
+ChargedParty ::= [APPLICATION 436] SEQUENCE
+{
+    imsi                       Imsi                            OPTIONAL, -- *m.m.
+    msisdn                             Msisdn                  OPTIONAL,         
+    publicUserId                       PublicUserId            OPTIONAL,
+    homeBid                            HomeBid                 OPTIONAL,
+    homeLocationDescription    HomeLocationDescription OPTIONAL,
+    imei                               Imei                            OPTIONAL,
+...
+}
+
+ChargedPartyEquipment ::= [APPLICATION 323] SEQUENCE
+{
+    equipmentIdType EquipmentIdType OPTIONAL, -- *m.m.
+    equipmentId     EquipmentId     OPTIONAL, -- *m.m.
+...
+}
+ChargedPartyHomeIdentification ::= [APPLICATION 313] SEQUENCE
+{
+    homeIdType     HomeIdType     OPTIONAL, -- *m.m.
+    homeIdentifier HomeIdentifier OPTIONAL, -- *m.m.
+...
+}
+
+ChargedPartyHomeIdList ::= [APPLICATION 314] SEQUENCE OF
+                                             ChargedPartyHomeIdentification
+
+ChargedPartyIdentification ::= [APPLICATION 309] SEQUENCE
+{
+    chargedPartyIdType         ChargedPartyIdType         OPTIONAL, -- *m.m.
+    chargedPartyIdentifier     ChargedPartyIdentifier     OPTIONAL, -- *m.m.
+...
+}
+
+ChargedPartyIdentifier ::= [APPLICATION 287] AsciiString
+
+ChargedPartyIdList ::= [APPLICATION 310] SEQUENCE OF ChargedPartyIdentification
+
+ChargedPartyIdType ::= [APPLICATION 305] INTEGER
+
+ChargedPartyInformation ::= [APPLICATION 324] SEQUENCE
+{
+    chargedPartyIdList       ChargedPartyIdList        OPTIONAL, -- *m.m.
+    chargedPartyHomeIdList   ChargedPartyHomeIdList    OPTIONAL,
+    chargedPartyLocationList ChargedPartyLocationList  OPTIONAL,
+    chargedPartyEquipment    ChargedPartyEquipment     OPTIONAL,
+...
+}
+ChargedPartyLocation ::= [APPLICATION 320] SEQUENCE
+{
+    locationIdType     LocationIdType     OPTIONAL, -- *m.m.
+    locationIdentifier LocationIdentifier OPTIONAL, -- *m.m.
+...
+}
+ChargedPartyLocationList ::= [APPLICATION 321] SEQUENCE OF ChargedPartyLocation
+ChargedPartyStatus ::= [APPLICATION 67] INTEGER 
+
+ChargedUnits ::= [APPLICATION 68]  INTEGER 
+
+ChargeInformation ::= [APPLICATION 69] SEQUENCE
+{
+    chargedItem         ChargedItem         OPTIONAL, -- *m.m.
+    exchangeRateCode    ExchangeRateCode    OPTIONAL,
+    callTypeGroup       CallTypeGroup       OPTIONAL,
+    chargeDetailList    ChargeDetailList    OPTIONAL, -- *m.m.
+    taxInformation      TaxInformationList  OPTIONAL,
+    discountInformation DiscountInformation OPTIONAL,
+...
+}
+
+ChargeInformationList ::= [APPLICATION 70] SEQUENCE OF ChargeInformation
+
+ChargeRefundIndicator ::= [APPLICATION 344] INTEGER
+ChargeType ::= [APPLICATION 71] NumberString --(SIZE(2..3))
+
+ChargingId ::= [APPLICATION 72] INTEGER
+
+ChargingPoint ::= [APPLICATION 73]  AsciiString --(SIZE(1))
+
+ChargingTimeStamp ::= [APPLICATION 74]  DateTime
+
+ClirIndicator ::= [APPLICATION 75] INTEGER
+
+Commission ::= [APPLICATION 350] Charge
+CompletionTimeStamp ::= [APPLICATION 76] DateTime
+
+ContentChargingPoint ::= [APPLICATION 345] INTEGER
+ContentProvider ::= [APPLICATION 327] SEQUENCE
+{
+    contentProviderIdType     ContentProviderIdType     OPTIONAL, -- *m.m.
+    contentProviderIdentifier ContentProviderIdentifier OPTIONAL, -- *m.m.
+...
+}
+ContentProviderIdentifier ::= [APPLICATION 292] AsciiString
+
+ContentProviderIdList ::= [APPLICATION 328] SEQUENCE OF ContentProvider
+
+ContentProviderIdType ::= [APPLICATION 291] INTEGER
+
+ContentProviderName ::= [APPLICATION 334] AsciiString
+ContentServiceUsed ::= [APPLICATION 352] SEQUENCE
+{
+    contentTransactionCode       ContentTransactionCode       OPTIONAL, -- *m.m.
+    contentTransactionType       ContentTransactionType       OPTIONAL, -- *m.m.
+    objectType                   ObjectType                   OPTIONAL,
+    transactionDescriptionSupp   TransactionDescriptionSupp   OPTIONAL,
+    transactionShortDescription  TransactionShortDescription  OPTIONAL, -- *m.m.
+    transactionDetailDescription TransactionDetailDescription OPTIONAL,
+    transactionIdentifier         TransactionIdentifier        OPTIONAL, -- *m.m.
+    transactionAuthCode          TransactionAuthCode          OPTIONAL,
+    dataVolumeIncoming           DataVolumeIncoming           OPTIONAL,
+    dataVolumeOutgoing           DataVolumeOutgoing           OPTIONAL,
+    totalDataVolume              TotalDataVolume              OPTIONAL,
+    chargeRefundIndicator        ChargeRefundIndicator        OPTIONAL,
+    contentChargingPoint         ContentChargingPoint         OPTIONAL,
+    chargeInformationList        ChargeInformationList        OPTIONAL,
+    advisedChargeInformation     AdvisedChargeInformation     OPTIONAL,
+...
+}
+
+ContentServiceUsedList ::= [APPLICATION 285] SEQUENCE OF ContentServiceUsed
+ContentTransactionBasicInfo ::= [APPLICATION 304] SEQUENCE
+{
+    rapFileSequenceNumber      RapFileSequenceNumber      OPTIONAL,
+    orderPlacedTimeStamp       OrderPlacedTimeStamp       OPTIONAL,
+    requestedDeliveryTimeStamp RequestedDeliveryTimeStamp OPTIONAL,
+    actualDeliveryTimeStamp    ActualDeliveryTimeStamp    OPTIONAL,
+    totalTransactionDuration   TotalTransactionDuration   OPTIONAL,
+    transactionStatus          TransactionStatus          OPTIONAL,
+...
+}
+
+ContentTransactionCode ::= [APPLICATION 336] INTEGER
+ContentTransactionType ::= [APPLICATION 337] INTEGER
+CseInformation ::= [APPLICATION 79] OCTET STRING --(SIZE(1..40))
+
+CurrencyConversion ::= [APPLICATION 106] SEQUENCE
+{
+    exchangeRateCode      ExchangeRateCode      OPTIONAL, -- *m.m.
+    numberOfDecimalPlaces NumberOfDecimalPlaces OPTIONAL, -- *m.m.
+    exchangeRate          ExchangeRate          OPTIONAL, -- *m.m.
+...
+}
+
+CurrencyConversionList ::= [APPLICATION 80] SEQUENCE OF CurrencyConversion
+
+CustomerIdentifier ::= [APPLICATION 364] AsciiString
+
+CustomerIdType ::= [APPLICATION 363] INTEGER
+
+DataVolume ::= INTEGER 
+
+DataVolumeIncoming ::= [APPLICATION 250] DataVolume
+
+DataVolumeOutgoing ::= [APPLICATION 251] DataVolume
+
+--
+--  The following datatypes are used to denote timestamps.
+--  Each timestamp consists of a local timestamp and a
+--  corresponding UTC time offset. 
+--  Except for the timestamps used within the Batch Control 
+--  Information and the Audit Control Information 
+--  the UTC time offset is identified by a code referencing
+--  the UtcTimeOffsetInfo.
+--  
+--
+-- We start with the “short” datatype referencing the 
+-- UtcTimeOffsetInfo.
+-- 
+
+DateTime ::= SEQUENCE 
+{
+     -- 
+     -- Local timestamps are noted in the format
+     --
+     --     CCYYMMDDhhmmss
+     --
+     -- where CC  =  century  (‘19’, ‘20’,...)
+     --       YY  =  year     (‘00’ – ‘99’)
+     --       MM  =  month    (‘01’, ‘02’, ... , ‘12’)
+     --       DD  =  day      (‘01’, ‘02’, ... , ‘31’)
+     --       hh  =  hour     (‘00’, ‘01’, ... , ‘23’)
+     --       mm  =  minutes  (‘00’, ‘01’, ... , ‘59’)
+     --       ss  =  seconds  (‘00’, ‘01’, ... , ‘59’)
+     -- 
+    localTimeStamp     LocalTimeStamp    OPTIONAL, -- *m.m.
+    utcTimeOffsetCode  UtcTimeOffsetCode OPTIONAL, -- *m.m.
+...
+}
+
+--
+-- The following version is the “long” datatype
+-- containing the UTC time offset directly. 
+--
+
+DateTimeLong ::= SEQUENCE 
+{
+    localTimeStamp     LocalTimeStamp OPTIONAL, -- *m.m.
+    utcTimeOffset      UtcTimeOffset  OPTIONAL, -- *m.m.
+...
+}
+
+DefaultCallHandlingIndicator ::= [APPLICATION 87] INTEGER
+
+DepositTimeStamp ::= [APPLICATION 88] DateTime
+
+Destination ::= [APPLICATION 89] SEQUENCE
+{
+    calledNumber                CalledNumber           OPTIONAL,
+    dialledDigits               DialledDigits         OPTIONAL,
+    calledPlace                 CalledPlace           OPTIONAL,
+    calledRegion                CalledRegion          OPTIONAL,
+    sMSDestinationNumber        SMSDestinationNumber  OPTIONAL,
+...
+}
+
+DestinationNetwork ::= [APPLICATION 90] NetworkId 
+
+DialledDigits ::= [APPLICATION 279] AsciiString
+
+Discount ::= [APPLICATION 412] DiscountValue
+
+DiscountableAmount ::= [APPLICATION 423] AbsoluteAmount
+
+DiscountApplied ::= [APPLICATION 428] CHOICE 
+{
+    fixedDiscountValue    FixedDiscountValue, 
+    discountRate          DiscountRate,
+...
+}
+
+DiscountCode ::= [APPLICATION 91] INTEGER
+
+DiscountInformation ::= [APPLICATION 96] SEQUENCE
+{
+    discountCode        DiscountCode           OPTIONAL, -- *m.m.
+    discount            Discount               OPTIONAL,
+    discountableAmount  DiscountableAmount     OPTIONAL,
+...
+}
+
+Discounting ::= [APPLICATION 94] SEQUENCE
+{
+    discountCode    DiscountCode    OPTIONAL, -- *m.m.
+    discountApplied DiscountApplied OPTIONAL, -- *m.m.
+...
+}
+
+DiscountingList ::= [APPLICATION 95]  SEQUENCE OF Discounting
+
+DiscountRate ::= [APPLICATION 92] PercentageRate
+
+DiscountValue ::= AbsoluteAmount
+
+DistanceChargeBandCode ::= [APPLICATION 98] AsciiString --(SIZE(1))
+
+EarliestCallTimeStamp ::= [APPLICATION 101] DateTimeLong
+
+ElementId ::= [APPLICATION 437] AsciiString
+
+ElementType ::= [APPLICATION 438] INTEGER
+
+EquipmentId ::= [APPLICATION 290] AsciiString
+
+EquipmentIdType ::= [APPLICATION 322] INTEGER
+
+Esn ::= [APPLICATION 103] NumberString
+
+ExchangeRate ::= [APPLICATION 104] INTEGER
+
+ExchangeRateCode ::= [APPLICATION 105] Code
+
+FileAvailableTimeStamp ::= [APPLICATION 107] DateTimeLong
+
+FileCreationTimeStamp ::= [APPLICATION 108] DateTimeLong
+
+FileSequenceNumber ::= [APPLICATION 109] NumberString --(SIZE(5))
+
+FileTypeIndicator ::= [APPLICATION 110] AsciiString --(SIZE(1))
+
+FixedDiscountValue ::= [APPLICATION 411] DiscountValue
+
+Fnur ::= [APPLICATION 111] INTEGER
+
+GeographicalLocation ::= [APPLICATION 113]  SEQUENCE
+{
+    servingNetwork              ServingNetwork                 OPTIONAL,
+    servingBid                  ServingBid                     OPTIONAL,
+    servingLocationDescription  ServingLocationDescription  OPTIONAL,
+...
+}
+
+GprsBasicCallInformation ::= [APPLICATION 114] SEQUENCE
+{
+    gprsChargeableSubscriber    GprsChargeableSubscriber OPTIONAL, -- *m.m.
+    rapFileSequenceNumber       RapFileSequenceNumber    OPTIONAL,
+    gprsDestination             GprsDestination          OPTIONAL, -- *m.m.
+    callEventStartTimeStamp     CallEventStartTimeStamp  OPTIONAL, -- *m.m.
+    totalCallEventDuration      TotalCallEventDuration   OPTIONAL, -- *m.m.
+    causeForTerm                CauseForTerm             OPTIONAL,
+    partialTypeIndicator        PartialTypeIndicator     OPTIONAL,
+    pDPContextStartTimestamp    PDPContextStartTimestamp OPTIONAL,
+    networkInitPDPContext       NetworkInitPDPContext    OPTIONAL,
+    chargingId                  ChargingId               OPTIONAL, -- *m.m.
+...
+}
+
+GprsChargeableSubscriber ::= [APPLICATION 115] SEQUENCE
+{
+    chargeableSubscriber        ChargeableSubscriber    OPTIONAL,
+    pdpAddress                  PdpAddress              OPTIONAL,
+    networkAccessIdentifier     NetworkAccessIdentifier OPTIONAL,
+...
+}
+
+GprsDestination ::= [APPLICATION 116] SEQUENCE
+{
+    accessPointNameNI           AccessPointNameNI      OPTIONAL, -- *m.m.
+    accessPointNameOI           AccessPointNameOI      OPTIONAL,
+...
+}
+
+GprsLocationInformation ::= [APPLICATION 117] SEQUENCE
+{
+    gprsNetworkLocation         GprsNetworkLocation     OPTIONAL, -- *m.m.
+    homeLocationInformation     HomeLocationInformation OPTIONAL,
+    geographicalLocation        GeographicalLocation    OPTIONAL, 
+...
+} 
+
+GprsNetworkLocation ::= [APPLICATION 118] SEQUENCE
+{
+    recEntity                   RecEntityCodeList OPTIONAL, -- *m.m.
+    locationArea                LocationArea      OPTIONAL,
+    cellId                      CellId            OPTIONAL,
+...
+}
+
+GprsServiceUsed ::= [APPLICATION 121]  SEQUENCE
+{
+    iMSSignallingContext        IMSSignallingContext  OPTIONAL,
+    dataVolumeIncoming          DataVolumeIncoming    OPTIONAL, -- *m.m.
+    dataVolumeOutgoing          DataVolumeOutgoing    OPTIONAL, -- *m.m.
+    chargeInformationList       ChargeInformationList OPTIONAL, -- *m.m.
+...
+}
+
+GsmChargeableSubscriber ::= [APPLICATION 286] SEQUENCE
+{
+    imsi     Imsi   OPTIONAL,
+    msisdn   Msisdn OPTIONAL,
+...
+}
+
+GuaranteedBitRate ::= [APPLICATION 420] OCTET STRING --(SIZE (1))
+
+HomeBid ::= [APPLICATION 122]  Bid
+
+HomeIdentifier ::= [APPLICATION 288] AsciiString
+
+HomeIdType ::= [APPLICATION 311] INTEGER
+
+HomeLocationDescription ::= [APPLICATION 413] LocationDescription
+
+HomeLocationInformation ::= [APPLICATION 123] SEQUENCE
+{
+    homeBid                     HomeBid                        OPTIONAL, -- *m.m.
+    homeLocationDescription     HomeLocationDescription        OPTIONAL, -- *m.m.
+...
+}
+
+HorizontalAccuracyDelivered ::= [APPLICATION 392] INTEGER
+
+HorizontalAccuracyRequested ::= [APPLICATION 385] INTEGER
+
+HSCSDIndicator ::= [APPLICATION 424] AsciiString --(SIZE(1))
+
+Imei ::= [APPLICATION 128] BCDString --(SIZE(7..8))
+
+ImeiOrEsn ::= [APPLICATION 429] CHOICE 
+{
+    imei  Imei,
+    esn   Esn,
+...
+} 
+
+Imsi ::= [APPLICATION 129] BCDString --(SIZE(3..8))
+
+IMSSignallingContext ::= [APPLICATION 418] INTEGER
+
+InternetServiceProvider ::= [APPLICATION 329] SEQUENCE
+{
+    ispIdType        IspIdType        OPTIONAL, -- *m.m.
+    ispIdentifier    IspIdentifier    OPTIONAL, -- *m.m.
+...
+}
+InternetServiceProviderIdList ::= [APPLICATION 330] SEQUENCE OF InternetServiceProvider
+
+IspIdentifier ::= [APPLICATION 294] AsciiString
+IspIdType ::= [APPLICATION 293] INTEGER
+
+ISPList ::= [APPLICATION 378] SEQUENCE OF InternetServiceProvider
+
+NetworkIdType ::= [APPLICATION 331] INTEGER
+
+NetworkIdentifier ::= [APPLICATION 295] AsciiString
+
+Network ::= [APPLICATION 332] SEQUENCE
+{
+    networkIdType     NetworkIdType     OPTIONAL, -- *m.m.
+    networkIdentifier NetworkIdentifier OPTIONAL, -- *m.m.
+...
+}
+NetworkList ::= [APPLICATION 333] SEQUENCE OF Network
+LatestCallTimeStamp ::= [APPLICATION 133] DateTimeLong
+
+LCSQosDelivered ::= [APPLICATION 390] SEQUENCE
+{
+    lCSTransactionStatus          LCSTransactionStatus        OPTIONAL,
+    horizontalAccuracyDelivered   HorizontalAccuracyDelivered OPTIONAL,
+    verticalAccuracyDelivered     VerticalAccuracyDelivered   OPTIONAL,
+    responseTime                  ResponseTime                OPTIONAL,
+    positioningMethod             PositioningMethod           OPTIONAL,
+    trackingPeriod                TrackingPeriod              OPTIONAL,
+    trackingFrequency             TrackingFrequency           OPTIONAL,
+    ageOfLocation                 AgeOfLocation               OPTIONAL,
+...
+}
+
+LCSQosRequested ::= [APPLICATION 383] SEQUENCE
+{
+    lCSRequestTimestamp           LCSRequestTimestamp         OPTIONAL, -- *m.m.
+    horizontalAccuracyRequested   HorizontalAccuracyRequested OPTIONAL,
+    verticalAccuracyRequested     VerticalAccuracyRequested   OPTIONAL,
+    responseTimeCategory          ResponseTimeCategory        OPTIONAL,
+    trackingPeriod                TrackingPeriod              OPTIONAL,
+    trackingFrequency             TrackingFrequency           OPTIONAL,
+...
+}
+
+LCSRequestTimestamp ::= [APPLICATION 384] DateTime
+
+LCSSPIdentification ::= [APPLICATION 375] SEQUENCE
+{
+ contentProviderIdType         ContentProviderIdType     OPTIONAL, -- *m.m.
+ contentProviderIdentifier     ContentProviderIdentifier OPTIONAL, -- *m.m.
+...
+}
+
+LCSSPIdentificationList ::= [APPLICATION 374] SEQUENCE OF LCSSPIdentification
+
+LCSSPInformation ::= [APPLICATION 373] SEQUENCE
+{
+    lCSSPIdentificationList       LCSSPIdentificationList OPTIONAL, -- *m.m.
+    iSPList                       ISPList                 OPTIONAL,
+    networkList                   NetworkList             OPTIONAL,
+...
+}
+
+LCSTransactionStatus ::= [APPLICATION 391] INTEGER
+
+LocalCurrency ::= [APPLICATION 135] Currency
+
+LocalTimeStamp ::= [APPLICATION 16] NumberString --(SIZE(14))
+
+LocationArea ::= [APPLICATION 136] INTEGER 
+
+LocationDescription ::= AsciiString
+
+LocationIdentifier ::= [APPLICATION 289] AsciiString
+
+LocationIdType ::= [APPLICATION 315] INTEGER
+
+LocationInformation ::= [APPLICATION 138]  SEQUENCE
+{
+    networkLocation             NetworkLocation         OPTIONAL, -- *m.m.
+    homeLocationInformation     HomeLocationInformation OPTIONAL,
+    geographicalLocation        GeographicalLocation    OPTIONAL,
+...
+} 
+
+LocationServiceUsage ::= [APPLICATION 382] SEQUENCE
+{
+    lCSQosRequested               LCSQosRequested       OPTIONAL, -- *m.m.
+    lCSQosDelivered               LCSQosDelivered       OPTIONAL,
+    chargingTimeStamp             ChargingTimeStamp     OPTIONAL,
+    chargeInformationList         ChargeInformationList OPTIONAL, -- *m.m.
+...
+}
+
+MaximumBitRate ::= [APPLICATION 421] OCTET STRING --(SIZE (1))
+
+Mdn ::= [APPLICATION 253] NumberString
+
+MessageDescription ::= [APPLICATION 142] AsciiString
+
+MessageDescriptionCode ::= [APPLICATION 141] Code
+
+MessageDescriptionInformation ::= [APPLICATION 143] SEQUENCE
+{
+    messageDescriptionCode MessageDescriptionCode OPTIONAL, -- *m.m.
+    messageDescription     MessageDescription     OPTIONAL, -- *m.m.
+...
+}
+
+MessageStatus ::= [APPLICATION 144] INTEGER
+
+MessageType ::= [APPLICATION 145] INTEGER
+
+MessagingEventService ::= [APPLICATION 439] INTEGER
+
+Min ::= [APPLICATION 146] NumberString --(SIZE(2..15)) 
+
+MinChargeableSubscriber ::= [APPLICATION 254] SEQUENCE
+{
+    min     Min    OPTIONAL, -- *m.m.
+    mdn     Mdn    OPTIONAL,
+...
+}
+
+MoBasicCallInformation ::= [APPLICATION 147] SEQUENCE
+{
+    chargeableSubscriber        ChargeableSubscriber    OPTIONAL, -- *m.m.
+    rapFileSequenceNumber       RapFileSequenceNumber   OPTIONAL,
+    destination                 Destination             OPTIONAL,
+    destinationNetwork          DestinationNetwork      OPTIONAL,
+    callEventStartTimeStamp     CallEventStartTimeStamp OPTIONAL, -- *m.m.
+    totalCallEventDuration      TotalCallEventDuration  OPTIONAL, -- *m.m.
+    simToolkitIndicator         SimToolkitIndicator     OPTIONAL,
+    causeForTerm                CauseForTerm            OPTIONAL,
+...
+}
+
+MobileSessionService ::= [APPLICATION 440] INTEGER      
+
+Msisdn ::= [APPLICATION 152] BCDString --(SIZE(1..9))
+
+MtBasicCallInformation ::= [APPLICATION 153] SEQUENCE
+{
+    chargeableSubscriber        ChargeableSubscriber    OPTIONAL, -- *m.m.
+    rapFileSequenceNumber       RapFileSequenceNumber   OPTIONAL,
+    callOriginator              CallOriginator          OPTIONAL,
+    originatingNetwork          OriginatingNetwork      OPTIONAL,
+    callEventStartTimeStamp     CallEventStartTimeStamp OPTIONAL, -- *m.m.
+    totalCallEventDuration      TotalCallEventDuration  OPTIONAL, -- *m.m.
+    simToolkitIndicator         SimToolkitIndicator     OPTIONAL,
+    causeForTerm                CauseForTerm            OPTIONAL,
+...
+}
+
+NetworkAccessIdentifier ::= [APPLICATION 417] AsciiString
+
+NetworkElement ::= [APPLICATION 441]  SEQUENCE
+{
+elementType             ElementType  OPTIONAL, -- *m.m.
+elementId               ElementId    OPTIONAL, -- *m.m.
+...
+}
+
+NetworkElementList ::= [APPLICATION 442] SEQUENCE OF NetworkElement
+
+NetworkId ::= AsciiString --(SIZE(1..6))
+
+NetworkInitPDPContext ::= [APPLICATION 245] INTEGER
+
+NetworkLocation ::= [APPLICATION 156]  SEQUENCE
+{
+    recEntityCode               RecEntityCode OPTIONAL, -- *m.m.
+    callReference               CallReference OPTIONAL,
+    locationArea                LocationArea  OPTIONAL,
+    cellId                      CellId        OPTIONAL,
+...
+}
+
+NonChargedNumber ::= [APPLICATION 402] AsciiString
+
+NonChargedParty ::= [APPLICATION 443]  SEQUENCE
+{
+    nonChargedPartyNumber       NonChargedPartyNumber   OPTIONAL,
+    nonChargedPublicUserId      NonChargedPublicUserId OPTIONAL,
+...
+}
+
+NonChargedPartyNumber ::= [APPLICATION 444] AddressStringDigits
+
+NonChargedPublicUserId ::= [APPLICATION 445] AsciiString 
+
+NumberOfDecimalPlaces ::= [APPLICATION 159] INTEGER
+
+ObjectType ::= [APPLICATION 281] INTEGER
+
+OperatorSpecInfoList ::= [APPLICATION 162] SEQUENCE OF OperatorSpecInformation
+
+OperatorSpecInformation ::= [APPLICATION 163] AsciiString
+
+OrderPlacedTimeStamp ::= [APPLICATION 300] DateTime
+
+OriginatingNetwork ::= [APPLICATION 164] NetworkId 
+
+PacketDataProtocolAddress ::= [APPLICATION 165] AsciiString 
+
+PaidIndicator ::= [APPLICATION 346] INTEGER
+PartialTypeIndicator ::=  [APPLICATION 166] AsciiString --(SIZE(1))
+
+PaymentMethod ::= [APPLICATION 347] INTEGER
+
+PdpAddress ::= [APPLICATION 167] PacketDataProtocolAddress
+
+PDPContextStartTimestamp ::= [APPLICATION 260] DateTime
+
+PlmnId ::= [APPLICATION 169] AsciiString --(SIZE(5))
+
+PositioningMethod ::= [APPLICATION 395] INTEGER
+
+PriorityCode ::= [APPLICATION 170] INTEGER
+
+PublicUserId ::= [APPLICATION 446] AsciiString 
+
+RapFileSequenceNumber ::= [APPLICATION 181]  FileSequenceNumber
+
+RecEntityCode ::= [APPLICATION 184] Code
+
+RecEntityCodeList ::= [APPLICATION 185] SEQUENCE OF RecEntityCode
+
+RecEntityId ::= [APPLICATION 400] AsciiString
+
+RecEntityInfoList ::= [APPLICATION 188] SEQUENCE OF RecEntityInformation
+
+RecEntityInformation ::= [APPLICATION 183] SEQUENCE
+{
+    recEntityCode  RecEntityCode OPTIONAL, -- *m.m.
+    recEntityType  RecEntityType OPTIONAL, -- *m.m.
+    recEntityId    RecEntityId   OPTIONAL, -- *m.m.
+...
+}
+RecEntityType ::= [APPLICATION 186] INTEGER
+
+Recipient ::= [APPLICATION 182]  PlmnId
+
+ReleaseVersionNumber ::= [APPLICATION 189] INTEGER
+
+RequestedDeliveryTimeStamp ::= [APPLICATION 301] DateTime
+
+ResponseTime ::= [APPLICATION 394] INTEGER
+
+ResponseTimeCategory ::= [APPLICATION 387] INTEGER
+
+ScuBasicInformation ::= [APPLICATION 191] SEQUENCE
+{
+    chargeableSubscriber      ScuChargeableSubscriber    OPTIONAL, -- *m.m.
+    chargedPartyStatus        ChargedPartyStatus         OPTIONAL, -- *m.m.
+    nonChargedNumber          NonChargedNumber           OPTIONAL, -- *m.m.
+    clirIndicator             ClirIndicator              OPTIONAL,
+    originatingNetwork        OriginatingNetwork         OPTIONAL,
+    destinationNetwork        DestinationNetwork         OPTIONAL,
+...
+}
+
+ScuChargeType ::= [APPLICATION 192]  SEQUENCE
+{
+    messageStatus               MessageStatus          OPTIONAL, -- *m.m.
+    priorityCode                PriorityCode           OPTIONAL, -- *m.m.
+    distanceChargeBandCode      DistanceChargeBandCode OPTIONAL,
+    messageType                 MessageType            OPTIONAL, -- *m.m.
+    messageDescriptionCode      MessageDescriptionCode OPTIONAL, -- *m.m.
+...
+}
+
+ScuTimeStamps ::= [APPLICATION 193]  SEQUENCE
+{
+    depositTimeStamp            DepositTimeStamp    OPTIONAL, -- *m.m.
+    completionTimeStamp         CompletionTimeStamp OPTIONAL, -- *m.m.
+    chargingPoint               ChargingPoint       OPTIONAL, -- *m.m.
+...
+}
+
+ScuChargeableSubscriber ::= [APPLICATION 430] CHOICE 
+{
+    gsmChargeableSubscriber    GsmChargeableSubscriber,
+    minChargeableSubscriber    MinChargeableSubscriber,
+...
+}
+
+Sender ::= [APPLICATION 196]  PlmnId
+
+ServiceStartTimestamp ::= [APPLICATION 447] DateTime
+
+ServingBid ::= [APPLICATION 198]  Bid
+
+ServingLocationDescription ::= [APPLICATION 414] LocationDescription
+
+ServingNetwork ::= [APPLICATION 195]  AsciiString
+
+ServingPartiesInformation ::= [APPLICATION 335] SEQUENCE
+{
+  contentProviderName           ContentProviderName           OPTIONAL, -- *m.m.
+  contentProviderIdList         ContentProviderIdList         OPTIONAL,
+  internetServiceProviderIdList InternetServiceProviderIdList OPTIONAL,
+  networkList                   NetworkList                   OPTIONAL,
+...
+}
+
+SessionChargeInfoList ::= [APPLICATION 448] SEQUENCE OF SessionChargeInformation
+
+SessionChargeInformation ::= [APPLICATION 449] SEQUENCE
+{
+chargedItem                    ChargedItem              OPTIONAL, -- *m.m.    
+exchangeRateCode               ExchangeRateCode         OPTIONAL,
+       callTypeGroup           CallTypeGroup            OPTIONAL, -- *m.m.
+       chargeDetailList        ChargeDetailList         OPTIONAL, -- *m.m.
+       taxInformationList      TaxInformationList       OPTIONAL,
+...
+}         
+SimChargeableSubscriber ::= [APPLICATION 199] SEQUENCE
+{
+    imsi     Imsi   OPTIONAL, -- *m.m.
+    msisdn   Msisdn OPTIONAL,
+...
+}
+
+SimToolkitIndicator ::= [APPLICATION 200] AsciiString --(SIZE(1)) 
+
+SMSDestinationNumber ::= [APPLICATION 419] AsciiString
+
+SMSOriginator ::= [APPLICATION 425] AsciiString
+
+SpecificationVersionNumber  ::= [APPLICATION 201] INTEGER
+
+SsParameters ::= [APPLICATION 204] AsciiString --(SIZE(1..40))
+
+SupplServiceActionCode ::= [APPLICATION 208] INTEGER
+
+SupplServiceCode ::= [APPLICATION 209] HexString --(SIZE(2))
+
+SupplServiceUsed ::= [APPLICATION 206] SEQUENCE
+{
+    supplServiceCode       SupplServiceCode       OPTIONAL, -- *m.m.
+    supplServiceActionCode SupplServiceActionCode OPTIONAL, -- *m.m.
+    ssParameters           SsParameters           OPTIONAL,
+    chargingTimeStamp      ChargingTimeStamp      OPTIONAL,
+    chargeInformation      ChargeInformation      OPTIONAL,
+    basicServiceCodeList   BasicServiceCodeList   OPTIONAL,
+...
+}
+
+TapCurrency ::= [APPLICATION 210] Currency
+
+TapDecimalPlaces ::= [APPLICATION 244] INTEGER
+
+TaxableAmount ::= [APPLICATION 398] AbsoluteAmount
+
+Taxation ::= [APPLICATION 216] SEQUENCE
+{
+    taxCode      TaxCode      OPTIONAL, -- *m.m.
+    taxType      TaxType      OPTIONAL, -- *m.m.
+    taxRate      TaxRate      OPTIONAL,
+    chargeType   ChargeType   OPTIONAL,
+    taxIndicator TaxIndicator OPTIONAL,
+...
+}
+
+TaxationList ::= [APPLICATION 211]  SEQUENCE OF Taxation
+
+TaxCode ::= [APPLICATION 212] INTEGER
+
+TaxIndicator ::= [APPLICATION 432] AsciiString --(SIZE(1))
+
+TaxInformation ::= [APPLICATION 213] SEQUENCE
+{
+    taxCode          TaxCode       OPTIONAL, -- *m.m.
+    taxValue         TaxValue      OPTIONAL, -- *m.m.
+    taxableAmount    TaxableAmount OPTIONAL,
+...
+}
+
+TaxInformationList ::= [APPLICATION 214]  SEQUENCE OF TaxInformation
+
+-- The TaxRate item is of a fixed length to ensure that the full 5 
+-- decimal places is provided.
+
+TaxRate ::= [APPLICATION 215] NumberString --(SIZE(7))
+
+TaxType ::= [APPLICATION 217] AsciiString --(SIZE(2))
+
+TaxValue ::= [APPLICATION 397] AbsoluteAmount
+
+TeleServiceCode ::= [APPLICATION 218] HexString --(SIZE(2))
+
+ThirdPartyInformation ::= [APPLICATION 219]  SEQUENCE
+{
+    thirdPartyNumber            ThirdPartyNumber       OPTIONAL,
+    clirIndicator               ClirIndicator         OPTIONAL,
+...
+}
+
+ThirdPartyNumber ::= [APPLICATION 403] AddressStringDigits
+
+ThreeGcamelDestination ::= [APPLICATION 431] CHOICE
+{
+    camelDestinationNumber    CamelDestinationNumber,
+    gprsDestination           GprsDestination,
+...
+}
+
+TotalAdvisedCharge ::= [APPLICATION 356] AbsoluteAmount
+TotalAdvisedChargeRefund ::= [APPLICATION 357] AbsoluteAmount
+TotalAdvisedChargeValue ::= [APPLICATION 360] SEQUENCE
+{
+    advisedChargeCurrency    AdvisedChargeCurrency    OPTIONAL,
+    totalAdvisedCharge       TotalAdvisedCharge       OPTIONAL, -- *m.m.
+    totalAdvisedChargeRefund TotalAdvisedChargeRefund OPTIONAL,
+    totalCommission          TotalCommission          OPTIONAL,
+    totalCommissionRefund    TotalCommissionRefund    OPTIONAL,
+...
+}
+TotalAdvisedChargeValueList ::= [APPLICATION 361] SEQUENCE OF TotalAdvisedChargeValue
+
+TotalCallEventDuration ::= [APPLICATION 223] INTEGER 
+
+TotalCharge ::= [APPLICATION 415] AbsoluteAmount
+
+TotalChargeRefund ::= [APPLICATION 355] AbsoluteAmount
+TotalCommission ::= [APPLICATION 358] AbsoluteAmount
+TotalCommissionRefund ::= [APPLICATION 359] AbsoluteAmount
+TotalDataVolume ::= [APPLICATION 343] DataVolume
+TotalDiscountRefund ::= [APPLICATION 354] AbsoluteAmount
+TotalDiscountValue ::= [APPLICATION 225] AbsoluteAmount
+
+TotalTaxRefund ::= [APPLICATION 353] AbsoluteAmount
+TotalTaxValue ::= [APPLICATION 226] AbsoluteAmount
+
+TotalTransactionDuration ::= [APPLICATION 416] TotalCallEventDuration
+
+TrackedCustomerEquipment ::= [APPLICATION 381] SEQUENCE
+{
+    equipmentIdType               EquipmentIdType OPTIONAL, -- *m.m.
+    equipmentId                   EquipmentId     OPTIONAL, -- *m.m.
+...
+}
+
+TrackedCustomerHomeId ::= [APPLICATION 377] SEQUENCE
+{
+    homeIdType                    HomeIdType     OPTIONAL, -- *m.m.
+    homeIdentifier                HomeIdentifier OPTIONAL, -- *m.m.
+...
+}
+
+TrackedCustomerHomeIdList ::= [APPLICATION 376] SEQUENCE OF TrackedCustomerHomeId
+
+TrackedCustomerIdentification ::= [APPLICATION 372] SEQUENCE
+{
+    customerIdType                CustomerIdType     OPTIONAL, -- *m.m.
+    customerIdentifier            CustomerIdentifier OPTIONAL, -- *m.m.
+...
+}
+
+TrackedCustomerIdList ::= [APPLICATION 370] SEQUENCE OF TrackedCustomerIdentification
+
+TrackedCustomerInformation ::= [APPLICATION 367] SEQUENCE
+{
+    trackedCustomerIdList         TrackedCustomerIdList     OPTIONAL, -- *m.m.
+    trackedCustomerHomeIdList     TrackedCustomerHomeIdList OPTIONAL,
+    trackedCustomerLocList        TrackedCustomerLocList    OPTIONAL,
+    trackedCustomerEquipment      TrackedCustomerEquipment  OPTIONAL,
+...
+}
+
+TrackedCustomerLocation ::= [APPLICATION 380] SEQUENCE
+{
+    locationIdType                LocationIdType     OPTIONAL, -- *m.m.
+    locationIdentifier            LocationIdentifier OPTIONAL, -- *m.m.
+...
+}
+
+TrackedCustomerLocList ::= [APPLICATION 379] SEQUENCE OF TrackedCustomerLocation
+
+TrackingCustomerEquipment ::= [APPLICATION 371] SEQUENCE
+{
+    equipmentIdType               EquipmentIdType OPTIONAL, -- *m.m.
+    equipmentId                   EquipmentId     OPTIONAL, -- *m.m.
+...
+}
+
+TrackingCustomerHomeId ::= [APPLICATION 366] SEQUENCE
+{
+    homeIdType                    HomeIdType     OPTIONAL, -- *m.m.
+    homeIdentifier                HomeIdentifier OPTIONAL, -- *m.m.
+...
+}
+
+TrackingCustomerHomeIdList ::= [APPLICATION 365] SEQUENCE OF TrackingCustomerHomeId
+
+TrackingCustomerIdentification ::= [APPLICATION 362] SEQUENCE
+{
+    customerIdType                CustomerIdType     OPTIONAL, -- *m.m.
+    customerIdentifier            CustomerIdentifier OPTIONAL, -- *m.m.
+...
+}
+
+TrackingCustomerIdList ::= [APPLICATION 299] SEQUENCE OF TrackingCustomerIdentification
+
+TrackingCustomerInformation ::= [APPLICATION 298] SEQUENCE
+{
+    trackingCustomerIdList        TrackingCustomerIdList     OPTIONAL, -- *m.m.
+    trackingCustomerHomeIdList    TrackingCustomerHomeIdList OPTIONAL,
+    trackingCustomerLocList       TrackingCustomerLocList    OPTIONAL,
+    trackingCustomerEquipment     TrackingCustomerEquipment  OPTIONAL,
+...
+}
+
+TrackingCustomerLocation ::= [APPLICATION 369] SEQUENCE
+{
+    locationIdType                LocationIdType     OPTIONAL, -- *m.m.
+    locationIdentifier            LocationIdentifier OPTIONAL, -- *m.m.
+...
+}
+
+TrackingCustomerLocList ::= [APPLICATION 368] SEQUENCE OF TrackingCustomerLocation
+
+TrackingFrequency ::= [APPLICATION 389] INTEGER
+
+TrackingPeriod ::= [APPLICATION 388] INTEGER
+
+TransactionAuthCode ::= [APPLICATION 342] AsciiString
+TransactionDescriptionSupp ::= [APPLICATION 338] INTEGER
+TransactionDetailDescription ::= [APPLICATION 339] AsciiString
+
+TransactionIdentifier ::= [APPLICATION 341] AsciiString
+TransactionShortDescription ::= [APPLICATION 340] AsciiString
+TransactionStatus ::= [APPLICATION 303] INTEGER
+
+TransferCutOffTimeStamp ::= [APPLICATION 227] DateTimeLong
+
+TransparencyIndicator ::= [APPLICATION 228] INTEGER
+
+UserProtocolIndicator ::= [APPLICATION 280] INTEGER
+
+UtcTimeOffset ::= [APPLICATION 231] AsciiString --(SIZE(5))
+
+UtcTimeOffsetCode ::= [APPLICATION 232] Code
+
+UtcTimeOffsetInfo ::= [APPLICATION 233] SEQUENCE
+{
+    utcTimeOffsetCode   UtcTimeOffsetCode OPTIONAL, -- *m.m.
+    utcTimeOffset       UtcTimeOffset     OPTIONAL, -- *m.m.
+...
+}
+
+UtcTimeOffsetInfoList ::= [APPLICATION 234]  SEQUENCE OF UtcTimeOffsetInfo
+
+VerticalAccuracyDelivered ::= [APPLICATION 393] INTEGER
+
+VerticalAccuracyRequested ::= [APPLICATION 386] INTEGER
+
+
+--
+-- Tagged common data types
+--
+
+--
+-- The AbsoluteAmount data type is used to 
+-- encode absolute revenue amounts.
+-- The accuracy of all absolute amount values is defined
+-- by the value of TapDecimalPlaces within the group
+-- AccountingInfo for the entire TAP batch.
+-- Note, that only amounts greater than or equal to zero are allowed.
+-- The decimal number representing the amount is 
+-- derived from the encoded integer 
+-- value by division by 10^TapDecimalPlaces.
+-- for example for TapDecimalPlaces = 3 the following values
+-- will be derived:
+--       0   represents    0.000
+--      12   represents    0.012
+--    1234   represents    1.234
+-- for TapDecimalPlaces = 5 the following values will be
+-- derived:
+--       0   represents    0.00000
+--    1234   represents    0.01234
+--  123456   represents    1.23456
+-- This data type is used to encode (total) 
+-- charges, (total) discount values and 
+-- (total) tax values. 
+-- 
+AbsoluteAmount ::= INTEGER 
+
+Bid ::=  AsciiString --(SIZE(5))
+
+Code ::= INTEGER
+
+--
+-- Non-tagged common data types
+--
+--
+-- Recommended common data types to be used for file encoding:
+--
+-- The following definitions should be used for TAP file creation instead of
+-- the default specifications (OCTET STRING)
+--
+--    AsciiString ::= VisibleString
+--
+--    Currency ::= VisibleString
+--
+--    HexString ::= VisibleString
+--
+--    NumberString ::= NumericString
+--
+--    AsciiString contains visible ISO 646 characters.
+--    Leading and trailing spaces must be discarded during processing.
+--    An AsciiString cannot contain only spaces.
+
+AsciiString ::= OCTET STRING
+
+--
+-- The BCDString data type (Binary Coded Decimal String) is used to represent
+-- several digits from 0 through 9, a, b, c, d, e.
+-- Two digits are encoded per octet.  The four leftmost bits of the octet represent
+-- the first digit while the four remaining bits represent the following digit.  
+-- A single f must be used as a filler when the total number of digits to be 
+-- encoded is odd.
+-- No other filler is allowed.
+
+BCDString ::= OCTET STRING
+
+
+--
+-- The currency codes from ISO 4217
+-- are used to identify a currency 
+--
+Currency ::= OCTET STRING
+
+--
+-- HexString contains ISO 646 characters from 0 through 9, A, B, C, D, E, F.
+--
+
+HexString ::= OCTET STRING
+
+--
+-- NumberString contains ISO 646 characters from 0 through 9.
+--
+
+NumberString ::= OCTET STRING
+
+
+--
+-- The PercentageRate data type is used to
+-- encode percentage rates with an accuracy of 2 decimal places. 
+-- This data type is used to encode discount rates.
+-- The decimal number representing the percentage
+-- rate is obtained by dividing the integer value by 100
+-- Examples:
+--
+--     1500  represents  15.00 percent
+--     1     represents   0.01 percent
+--
+PercentageRate ::= INTEGER 
+
+
+-- END
+END
+}
+
+1;
diff --git a/FS/FS/cdr/huawei_softx3000.pm b/FS/FS/cdr/huawei_softx3000.pm
new file mode 100644 (file)
index 0000000..e66af43
--- /dev/null
@@ -0,0 +1,2689 @@
+package FS::cdr::huawei_softx3000;
+use base qw( FS::cdr );
+
+use strict;
+use vars qw( %info %TZ );
+use subs qw( ts24008_number TimeStamp );
+use Time::Local;
+use FS::Record qw( qsearch );
+use FS::cdr_calltype;
+
+#false laziness w/gsm_tap3_12.pm
+%TZ = (
+  '+0000' => 'XXX-0',
+  '+0100' => 'XXX-1',
+  '+0200' => 'XXX-2',
+  '+0300' => 'XXX-3',
+  '+0400' => 'XXX-4',
+  '+0500' => 'XXX-5',
+  '+0600' => 'XXX-6',
+  '+0700' => 'XXX-7',
+  '+0800' => 'XXX-8',
+  '+0900' => 'XXX-9',
+  '+1000' => 'XXX-10',
+  '+1100' => 'XXX-11',
+  '+1200' => 'XXX-12',
+  '-0000' => 'XXX+0',
+  '-0100' => 'XXX+1',
+  '-0200' => 'XXX+2',
+  '-0300' => 'XXX+3',
+  '-0400' => 'XXX+4',
+  '-0500' => 'XXX+5',
+  '-0600' => 'XXX+6',
+  '-0700' => 'XXX+7',
+  '-0800' => 'XXX+8',
+  '-0900' => 'XXX+9',
+  '-1000' => 'XXX+10',
+  '-1100' => 'XXX+11',
+  '-1200' => 'XXX+12',
+);
+
+%info = (
+  'name'          => 'Huawei SoftX3000', #V100R006C05 ?
+  'weight'        => 160,
+  'type'          => 'asn.1',
+  'import_fields' => [],
+  'asn_format'    => {
+    'spec' => _asn_spec(),
+    'macro'         => 'CallEventDataFile',
+    'header_buffer' => sub {
+      #my $CallEventDataFile = shift;
+
+      my %cdr_calltype = ( map { $_->calltypename => $_->calltypenum }
+                             qsearch('cdr_calltype', {})
+                         );
+
+      { cdr_calltype => \%cdr_calltype,
+      };
+
+    },
+    'arrayref'      => sub { shift->{'callEventRecords'} },
+    'row_callback'  => sub {
+      my( $row, $buffer ) = @_;
+      my @keys = keys %$row;
+      $buffer->{'key'} = $keys[0];
+    },
+    'map'           => {
+      'src'           => huawei_field('callingNumber', ts24008_number, ),
+
+      'dst'           => huawei_field('calledNumber',  ts24008_number, ),
+
+      'startdate'     => huawei_field(['answerTime','deliveryTime'], TimeStamp),
+      'answerdate'    => huawei_field(['answerTime','deliveryTime'], TimeStamp),
+      'enddate'       => huawei_field('releaseTime', TimeStamp),
+      'duration'      => huawei_field('callDuration'),
+      'billsec'       => huawei_field('callDuration'),
+      #'disposition'   => #diagnostics?
+      #'accountcode'
+      #'charged_party' => # 0 or 1, do something with this?
+      'calltypenum'   => sub {
+        my($rec, $buf) = @_;
+        my $key = $buf->{key};
+        $buf->{'cdr_calltype'}{ $key };
+      },
+      #'carrierid' =>
+    },
+
+  },
+);
+
+sub huawei_field {
+  my $field = shift;
+  my $decode = $_[0] ? shift : '';
+  return sub {
+    my($rec, $buf) = @_;
+
+    my $key = $buf->{key};
+
+    $field = ref($field) ? $field : [ $field ];
+    my $value = '';
+    foreach my $f (@$field) {
+      $value = $rec->{$key}{$f} and last;
+    }
+
+    $decode
+      ? &{ $decode }( $value )
+      : $value;
+
+  };
+}
+
+sub ts24008_number {
+  # This type contains the binary coded decimal representation of
+  # a directory number e.g. calling/called/connected/translated number.
+  # The encoding of the octet string is in accordance with the
+  # the elements "Calling party BCD number", "Called party BCD number"
+  # and "Connected number" defined in TS 24.008.
+  # This encoding includes type of number and number plan information
+  # together with a BCD encoded digit string.
+  # It may also contain both a presentation and screening indicator
+  # (octet 3a).
+  # For the avoidance of doubt, this field does not include
+  # octets 1 and 2, the element name and length, as this would be
+  # redundant.
+  #
+  #type id (per TS 24.008 page 490):
+  #          low nybble: "numbering plan identification"
+  #         high nybble: "type of number"
+  #                      0 unknown
+  #                      1 international
+  #                      2 national
+  #                      3 network specific
+  #                      4 dedicated access, short code
+  #                      5 reserved
+  #                      6 reserved
+  #                      7 reserved for extension
+  #                   (bit 8 "extension")
+  return sub {
+    my( $type_id, $value ) = unpack 'Ch*', shift;
+    $value =~ s/f$//; # If the called party BCD number contains an odd number
+                      # of digits, bits 5 to 8 of the last octet shall be
+                      # filled with an end mark coded as "1111".
+    $value;
+  };
+}
+
+sub TimeStamp {
+  # The contents of this field are a compact form of the UTCTime format
+  # containing local time plus an offset to universal time. Binary coded
+  # decimal encoding is employed for the digits to reduce the storage and
+  # transmission overhead
+  # e.g. YYMMDDhhmmssShhmm
+  # where
+  # YY    =    Year 00 to 99        BCD encoded
+  # MM    =    Month 01 to 12       BCD encoded
+  # DD    =    Day 01 to 31         BCD encoded
+  # hh    =    hour 00 to 23        BCD encoded
+  # mm    =    minute 00 to 59      BCD encoded
+  # ss    =    second 00 to 59      BCD encoded
+  # S     =    Sign 0 = "+", "-"    ASCII encoded
+  # hh    =    hour 00 to 23        BCD encoded
+  # mm    =    minute 00 to 59      BCD encoded
+  return sub {
+    my($year, $mon, $day, $hour, $min, $sec, $tz_sign, $tz_hour, $tz_min, $dst)=
+      unpack 'H2H2H2H2H2H2AH2H2C', shift;  
+    #warn "$year/$mon/$day $hour:$min:$sec $tz_sign$tz_hour$tz_min $dst\n";
+    return 0 unless $year; #y2100 bug
+    local($ENV{TZ}) = $TZ{ "$tz_sign$tz_hour$tz_min" };
+    timelocal($sec, $min, $hour, $day, $mon-1, $year);
+  };
+}
+
+sub _asn_spec {
+  <<'END';
+
+--DEFINITIONS IMPLICIT TAGS    ::=
+
+--BEGIN
+
+--------------------------------------------------------------------------------
+--
+--  CALL AND EVENT RECORDS
+--
+------------------------------------------------------------------------------
+--Font: verdana  8
+
+CallEventRecord    ::= CHOICE
+{
+    moCallRecord              [0] MOCallRecord,
+    mtCallRecord              [1] MTCallRecord,
+    roamingRecord             [2] RoamingRecord,
+    incGatewayRecord          [3] IncGatewayRecord,
+    outGatewayRecord          [4] OutGatewayRecord,
+    transitRecord             [5] TransitCallRecord,
+    moSMSRecord               [6] MOSMSRecord,
+    mtSMSRecord               [7] MTSMSRecord,
+    ssActionRecord           [10] SSActionRecord,
+    hlrIntRecord             [11] HLRIntRecord,
+    commonEquipRecord        [14] CommonEquipRecord,
+    recTypeExtensions        [15] ManagementExtensions,
+    termCAMELRecord          [16] TermCAMELRecord,
+    mtLCSRecord              [17] MTLCSRecord,
+    moLCSRecord              [18] MOLCSRecord,
+    niLCSRecord              [19] NILCSRecord,
+    forwardCallRecord       [100] MOCallRecord
+}
+
+MOCallRecord    ::= SET
+{
+    recordType                            [0] CallEventRecordType                          OPTIONAL,
+    servedIMSI                            [1] IMSI                                         OPTIONAL,
+    servedIMEI                            [2] IMEI                                         OPTIONAL,
+    servedMSISDN                          [3] MSISDN                                       OPTIONAL,
+    callingNumber                         [4] CallingNumber                                OPTIONAL,
+    calledNumber                          [5] CalledNumber                                 OPTIONAL,
+    translatedNumber                      [6] TranslatedNumber                             OPTIONAL,
+    connectedNumber                       [7] ConnectedNumber                              OPTIONAL,
+    roamingNumber                         [8] RoamingNumber                                OPTIONAL,
+    recordingEntity                       [9] RecordingEntity                              OPTIONAL,
+    mscIncomingROUTE                     [10] ROUTE                                        OPTIONAL,
+    mscOutgoingROUTE                     [11] ROUTE                                        OPTIONAL,
+    location                             [12] LocationAreaAndCell                          OPTIONAL,
+    changeOfLocation                     [13] SEQUENCE OF LocationChange                   OPTIONAL,
+    basicService                         [14] BasicServiceCode                             OPTIONAL,
+    transparencyIndicator                [15] TransparencyInd                              OPTIONAL,
+    changeOfService                      [16] SEQUENCE OF ChangeOfService                  OPTIONAL,
+    supplServicesUsed                    [17] SEQUENCE OF  SuppServiceUsed                 OPTIONAL,
+    aocParameters                        [18] AOCParameters                                OPTIONAL,
+    changeOfAOCParms                     [19] SEQUENCE OF AOCParmChange                    OPTIONAL,
+    msClassmark                          [20] Classmark                                    OPTIONAL,
+    changeOfClassmark                    [21] ChangeOfClassmark                            OPTIONAL,
+    seizureTime                          [22] TimeStamp                                    OPTIONAL,
+    answerTime                           [23] TimeStamp                                    OPTIONAL,
+    releaseTime                          [24] TimeStamp                                    OPTIONAL,
+    callDuration                         [25] CallDuration                                 OPTIONAL,
+    radioChanRequested                   [27] RadioChanRequested                           OPTIONAL,
+    radioChanUsed                        [28] TrafficChannel                               OPTIONAL,
+    changeOfRadioChan                    [29] ChangeOfRadioChannel                         OPTIONAL,
+    causeForTerm                         [30] CauseForTerm                                 OPTIONAL,
+    diagnostics                          [31] Diagnostics                                  OPTIONAL,
+    callReference                        [32] CallReference                                OPTIONAL,
+    sequenceNumber                       [33] SequenceNumber                               OPTIONAL,
+    additionalChgInfo                    [34] AdditionalChgInfo                            OPTIONAL,
+    recordExtensions                     [35] ManagementExtensions                         OPTIONAL,
+    gsm-SCFAddress                       [36] Gsm-SCFAddress                               OPTIONAL,
+    serviceKey                           [37] ServiceKey                                   OPTIONAL,
+    networkCallReference                 [38] NetworkCallReference                         OPTIONAL,
+    mSCAddress                           [39] MSCAddress                                   OPTIONAL,
+    cAMELInitCFIndicator                 [40] CAMELInitCFIndicator                         OPTIONAL,
+    defaultCallHandling                  [41] DefaultCallHandling                          OPTIONAL,
+    fnur                                 [45] Fnur                                         OPTIONAL,
+    aiurRequested                        [46] AiurRequested                                OPTIONAL,
+    speechVersionSupported               [49] SpeechVersionIdentifier                      OPTIONAL,
+    speechVersionUsed                    [50] SpeechVersionIdentifier                      OPTIONAL,
+    numberOfDPEncountered                [51] INTEGER                                      OPTIONAL,
+    levelOfCAMELService                  [52] LevelOfCAMELService                          OPTIONAL,
+    freeFormatData                       [53] FreeFormatData                               OPTIONAL,
+    cAMELCallLegInformation              [54] SEQUENCE OF CAMELInformation                 OPTIONAL,
+    freeFormatDataAppend                 [55] BOOLEAN                                      OPTIONAL,
+    defaultCallHandling-2                [56] DefaultCallHandling                          OPTIONAL,
+    gsm-SCFAddress-2                     [57] Gsm-SCFAddress                               OPTIONAL,
+    serviceKey-2                         [58] ServiceKey                                   OPTIONAL,
+    freeFormatData-2                     [59] FreeFormatData                               OPTIONAL,
+    freeFormatDataAppend-2               [60] BOOLEAN                                      OPTIONAL,
+    systemType                           [61] SystemType                                   OPTIONAL,
+    rateIndication                       [62] RateIndication                               OPTIONAL,
+    partialRecordType                    [69] PartialRecordType                            OPTIONAL,
+    guaranteedBitRate                    [70] GuaranteedBitRate                            OPTIONAL,
+    maximumBitRate                       [71] MaximumBitRate                               OPTIONAL,
+    modemType                           [139] ModemType                                    OPTIONAL,
+    classmark3                          [140] Classmark3                                   OPTIONAL,
+    chargedParty                        [141] ChargedParty                                 OPTIONAL,
+    originalCalledNumber                [142] OriginalCalledNumber                         OPTIONAL,
+    callingChargeAreaCode               [145] ChargeAreaCode                               OPTIONAL,
+    calledChargeAreaCode                [146] ChargeAreaCode                               OPTIONAL,
+    mscOutgoingCircuit                  [166] MSCCIC                                       OPTIONAL,
+    orgRNCorBSCId                       [167] RNCorBSCId                                   OPTIONAL,
+    orgMSCId                            [168] MSCId                                        OPTIONAL,
+    callEmlppPriority                   [170] EmlppPriority                                OPTIONAL,
+    callerDefaultEmlppPriority          [171] EmlppPriority                                OPTIONAL,
+    eaSubscriberInfo                    [174] EASubscriberInfo                             OPTIONAL,
+    selectedCIC                         [175] SelectedCIC                                  OPTIONAL,
+    optimalRoutingFlag                  [177] NULL                                         OPTIONAL,
+    optimalRoutingLateForwardFlag       [178] NULL                                         OPTIONAL,
+    optimalRoutingEarlyForwardFlag      [179] NULL                                         OPTIONAL,
+    portedflag                          [180] PortedFlag                                   OPTIONAL,
+    calledIMSI                          [181] IMSI                                         OPTIONAL,
+    globalAreaID                        [188] GAI                                          OPTIONAL,
+    changeOfglobalAreaID                [189] SEQUENCE OF ChangeOfglobalAreaID             OPTIONAL,
+    subscriberCategory                  [190] SubscriberCategory                           OPTIONAL,
+    firstmccmnc                         [192] MCCMNC                                       OPTIONAL,
+    intermediatemccmnc                  [193] MCCMNC                                       OPTIONAL,
+    lastmccmnc                          [194] MCCMNC                                       OPTIONAL,
+    cUGOutgoingAccessIndicator          [195] CUGOutgoingAccessIndicator                   OPTIONAL,
+    cUGInterlockCode                    [196] CUGInterlockCode                             OPTIONAL,
+    cUGOutgoingAccessUsed               [197] CUGOutgoingAccessUsed                        OPTIONAL,
+    cUGIndex                            [198] CUGIndex                                     OPTIONAL,
+    interactionWithIP                   [199] InteractionWithIP                            OPTIONAL,
+    hotBillingTag                       [200] HotBillingTag                                OPTIONAL,
+    setupTime                           [201] TimeStamp                                    OPTIONAL,
+    alertingTime                        [202] TimeStamp                                    OPTIONAL,
+    voiceIndicator                      [203] VoiceIndicator                               OPTIONAL,
+    bCategory                           [204] BCategory                                    OPTIONAL,
+    callType                            [205] CallType                                     OPTIONAL
+}
+
+--at moc     callingNumber is the same as served msisdn except basic msisdn != calling number such as MSP service
+
+MTCallRecord            ::= SET
+{
+    recordType                            [0] CallEventRecordType                          OPTIONAL,
+    servedIMSI                            [1] IMSI                                         OPTIONAL,
+    servedIMEI                            [2] IMEI                                         OPTIONAL,
+    servedMSISDN                          [3] CalledNumber                                 OPTIONAL,
+    callingNumber                         [4] CallingNumber                                OPTIONAL,
+    connectedNumber                       [5] ConnectedNumber                              OPTIONAL,
+    recordingEntity                       [6] RecordingEntity                              OPTIONAL,
+    mscIncomingROUTE                      [7] ROUTE                                        OPTIONAL,
+    mscOutgoingROUTE                      [8] ROUTE                                        OPTIONAL,
+    location                              [9] LocationAreaAndCell                          OPTIONAL,
+    changeOfLocation                     [10] SEQUENCE OF LocationChange                   OPTIONAL,
+    basicService                         [11] BasicServiceCode                             OPTIONAL,
+    transparencyIndicator                [12] TransparencyInd                              OPTIONAL,
+    changeOfService                      [13] SEQUENCE OF ChangeOfService                  OPTIONAL,
+    supplServicesUsed                    [14] SEQUENCE OF SuppServiceUsed                  OPTIONAL,
+    aocParameters                        [15] AOCParameters                                OPTIONAL,
+    changeOfAOCParms                     [16] SEQUENCE OF AOCParmChange                    OPTIONAL,
+    msClassmark                          [17] Classmark                                    OPTIONAL,
+    changeOfClassmark                    [18] ChangeOfClassmark                            OPTIONAL,
+    seizureTime                          [19] TimeStamp                                    OPTIONAL,
+    answerTime                           [20] TimeStamp                                    OPTIONAL,
+    releaseTime                          [21] TimeStamp                                    OPTIONAL,
+    callDuration                         [22] CallDuration                                 OPTIONAL,
+    radioChanRequested                   [24] RadioChanRequested                           OPTIONAL,
+    radioChanUsed                        [25] TrafficChannel                               OPTIONAL,
+    changeOfRadioChan                    [26] ChangeOfRadioChannel                         OPTIONAL,
+    causeForTerm                         [27] CauseForTerm                                 OPTIONAL,
+    diagnostics                          [28] Diagnostics                                  OPTIONAL,
+    callReference                        [29] CallReference                                OPTIONAL,
+    sequenceNumber                       [30] SequenceNumber                               OPTIONAL,
+    additionalChgInfo                    [31] AdditionalChgInfo                            OPTIONAL,
+    recordExtensions                     [32] ManagementExtensions                         OPTIONAL,
+    networkCallReference                 [33] NetworkCallReference                         OPTIONAL,
+    mSCAddress                           [34] MSCAddress                                   OPTIONAL,
+    fnur                                 [38] Fnur                                         OPTIONAL,
+    aiurRequested                        [39] AiurRequested                                OPTIONAL,
+    speechVersionSupported               [42] SpeechVersionIdentifier                      OPTIONAL,
+    speechVersionUsed                    [43] SpeechVersionIdentifier                      OPTIONAL,
+    gsm-SCFAddress                       [44] Gsm-SCFAddress                               OPTIONAL,
+    serviceKey                           [45] ServiceKey                                   OPTIONAL,
+    systemType                           [46] SystemType                                   OPTIONAL,
+    rateIndication                       [47] RateIndication                               OPTIONAL,
+    partialRecordType                    [54] PartialRecordType                            OPTIONAL,
+    guaranteedBitRate                    [55] GuaranteedBitRate                            OPTIONAL,
+    maximumBitRate                       [56] MaximumBitRate                               OPTIONAL,
+    initialCallAttemptFlag              [137] NULL                                         OPTIONAL,
+    ussdCallBackFlag                    [138] NULL                                         OPTIONAL,
+    modemType                           [139] ModemType                                    OPTIONAL,
+    classmark3                          [140] Classmark3                                   OPTIONAL,
+    chargedParty                        [141] ChargedParty                                 OPTIONAL,
+    originalCalledNumber                [142] OriginalCalledNumber                         OPTIONAL,
+    callingChargeAreaCode               [145]ChargeAreaCode                                OPTIONAL,
+    calledChargeAreaCode                [146]ChargeAreaCode                                OPTIONAL,
+    defaultCallHandling                 [150] DefaultCallHandling                          OPTIONAL,
+    freeFormatData                      [151] FreeFormatData                               OPTIONAL,
+    freeFormatDataAppend                [152] BOOLEAN                                      OPTIONAL,
+    numberOfDPEncountered               [153] INTEGER                                      OPTIONAL,
+    levelOfCAMELService                 [154] LevelOfCAMELService                          OPTIONAL,
+    roamingNumber                       [160] RoamingNumber                                OPTIONAL,
+    mscIncomingCircuit                  [166] MSCCIC                                       OPTIONAL,
+    orgRNCorBSCId                       [167] RNCorBSCId                                   OPTIONAL,
+    orgMSCId                            [168] MSCId                                        OPTIONAL,
+    callEmlppPriority                   [170] EmlppPriority                                OPTIONAL,
+    calledDefaultEmlppPriority          [171] EmlppPriority                                OPTIONAL,
+    eaSubscriberInfo                    [174] EASubscriberInfo                             OPTIONAL,
+    selectedCIC                         [175] SelectedCIC                                  OPTIONAL,
+    optimalRoutingFlag                  [177] NULL                                         OPTIONAL,
+    portedflag                          [180] PortedFlag                                   OPTIONAL,
+    globalAreaID                        [188] GAI                                          OPTIONAL,
+    changeOfglobalAreaID                [189] SEQUENCE OF ChangeOfglobalAreaID             OPTIONAL,
+    subscriberCategory                  [190] SubscriberCategory                           OPTIONAL,
+    firstmccmnc                         [192] MCCMNC                                       OPTIONAL,
+    intermediatemccmnc                  [193] MCCMNC                                       OPTIONAL,
+    lastmccmnc                          [194] MCCMNC                                       OPTIONAL,
+    cUGOutgoingAccessIndicator          [195] CUGOutgoingAccessIndicator                   OPTIONAL,
+    cUGInterlockCode                    [196] CUGInterlockCode                             OPTIONAL,
+    cUGIncomingAccessUsed               [197] CUGIncomingAccessUsed                        OPTIONAL,
+    cUGIndex                            [198] CUGIndex                                     OPTIONAL,
+    hotBillingTag                       [200] HotBillingTag                                OPTIONAL,
+    redirectingnumber                   [201] RedirectingNumber                            OPTIONAL,
+    redirectingcounter                  [202] RedirectingCounter                           OPTIONAL,
+    setupTime                           [203] TimeStamp                                    OPTIONAL,
+    alertingTime                        [204] TimeStamp                                    OPTIONAL,
+    calledNumber                        [205] CalledNumber                                 OPTIONAL,
+    voiceIndicator                      [206] VoiceIndicator                               OPTIONAL,
+    bCategory                           [207] BCategory                                    OPTIONAL,
+    callType                            [208] CallType                                     OPTIONAL
+}
+
+RoamingRecord            ::= SET
+{
+    recordType                            [0] CallEventRecordType                          OPTIONAL,
+    servedIMSI                            [1] IMSI                                         OPTIONAL,
+    servedMSISDN                          [2] MSISDN                                       OPTIONAL,
+    callingNumber                         [3] CallingNumber                                OPTIONAL,
+    roamingNumber                         [4] RoamingNumber                                OPTIONAL,
+    recordingEntity                       [5] RecordingEntity                              OPTIONAL,
+    mscIncomingROUTE                      [6] ROUTE                                        OPTIONAL,
+    mscOutgoingROUTE                      [7] ROUTE                                        OPTIONAL,
+    basicService                          [8] BasicServiceCode                             OPTIONAL,
+    transparencyIndicator                 [9] TransparencyInd                              OPTIONAL,
+    changeOfService                      [10] SEQUENCE OF ChangeOfService                  OPTIONAL,
+    supplServicesUsed                    [11] SEQUENCE OF  SuppServiceUsed                 OPTIONAL,
+    seizureTime                          [12] TimeStamp                                    OPTIONAL,
+    answerTime                           [13] TimeStamp                                    OPTIONAL,
+    releaseTime                          [14] TimeStamp                                    OPTIONAL,
+    callDuration                         [15] CallDuration                                 OPTIONAL,
+    causeForTerm                         [17] CauseForTerm                                 OPTIONAL,
+    diagnostics                          [18] Diagnostics                                  OPTIONAL,
+    callReference                        [19] CallReference                                OPTIONAL,
+    sequenceNumber                       [20] SequenceNumber                               OPTIONAL,
+    recordExtensions                     [21] ManagementExtensions                         OPTIONAL,
+    networkCallReference                 [22] NetworkCallReference                         OPTIONAL,
+    mSCAddress                           [23] MSCAddress                                   OPTIONAL,
+    partialRecordType                    [30] PartialRecordType                            OPTIONAL,
+    additionalChgInfo                   [133] AdditionalChgInfo                            OPTIONAL,
+    chargedParty                        [141] ChargedParty                                 OPTIONAL,
+    originalCalledNumber                [142] OriginalCalledNumber                         OPTIONAL,
+    callingChargeAreaCode               [145] ChargeAreaCode                               OPTIONAL,
+    calledChargeAreaCode                [146] ChargeAreaCode                               OPTIONAL,
+    mscOutgoingCircuit                  [166] MSCCIC                                       OPTIONAL,
+    mscIncomingCircuit                  [167] MSCCIC                                       OPTIONAL,
+    orgMSCId                            [168] MSCId                                        OPTIONAL,
+    callEmlppPriority                   [170] EmlppPriority                                OPTIONAL,
+    eaSubscriberInfo                    [174] EASubscriberInfo                             OPTIONAL,
+    selectedCIC                         [175] SelectedCIC                                  OPTIONAL,
+    optimalRoutingFlag                  [177] NULL                                         OPTIONAL,
+    subscriberCategory                  [190] SubscriberCategory                           OPTIONAL,
+    cUGOutgoingAccessIndicator          [195] CUGOutgoingAccessIndicator                   OPTIONAL,
+    cUGInterlockCode                    [196] CUGInterlockCode                             OPTIONAL,
+    hotBillingTag                       [200] HotBillingTag                                OPTIONAL
+}
+
+TermCAMELRecord    ::= SET
+{
+    recordtype                            [0] CallEventRecordType                          OPTIONAL,
+    servedIMSI                            [1] IMSI                                         OPTIONAL,
+    servedMSISDN                          [2] MSISDN                                       OPTIONAL,
+    recordingEntity                       [3] RecordingEntity                              OPTIONAL,
+    interrogationTime                     [4] TimeStamp                                    OPTIONAL,
+    destinationRoutingAddress             [5] DestinationRoutingAddress                    OPTIONAL,
+    gsm-SCFAddress                        [6] Gsm-SCFAddress                               OPTIONAL,
+    serviceKey                            [7] ServiceKey                                   OPTIONAL,
+    networkCallReference                  [8] NetworkCallReference                         OPTIONAL,
+    mSCAddress                            [9] MSCAddress                                   OPTIONAL,
+    defaultCallHandling                  [10] DefaultCallHandling                          OPTIONAL,
+    recordExtensions                     [11] ManagementExtensions                         OPTIONAL,
+    calledNumber                         [12] CalledNumber                                 OPTIONAL,
+    callingNumber                        [13] CallingNumber                                OPTIONAL,
+    mscIncomingROUTE                     [14] ROUTE                                        OPTIONAL,
+    mscOutgoingROUTE                     [15] ROUTE                                        OPTIONAL,
+    seizureTime                          [16] TimeStamp                                    OPTIONAL,
+    answerTime                           [17] TimeStamp                                    OPTIONAL,
+    releaseTime                          [18] TimeStamp                                    OPTIONAL,
+    callDuration                         [19] CallDuration                                 OPTIONAL,
+    causeForTerm                         [21] CauseForTerm                                 OPTIONAL,
+    diagnostics                          [22] Diagnostics                                  OPTIONAL,
+    callReference                        [23] CallReference                                OPTIONAL,
+    sequenceNumber                       [24] SequenceNumber                               OPTIONAL,
+    numberOfDPEncountered                [25] INTEGER                                      OPTIONAL,
+    levelOfCAMELService                  [26] LevelOfCAMELService                          OPTIONAL,
+    freeFormatData                       [27] FreeFormatData                               OPTIONAL,
+    cAMELCallLegInformation              [28] SEQUENCE OF CAMELInformation                 OPTIONAL,
+    freeFormatDataAppend                 [29] BOOLEAN                                      OPTIONAL,
+    mscServerIndication                  [30] BOOLEAN                                      OPTIONAL,
+    defaultCallHandling-2                [31] DefaultCallHandling                          OPTIONAL,
+    gsm-SCFAddress-2                     [32] Gsm-SCFAddress                               OPTIONAL,
+    serviceKey-2                         [33] ServiceKey                                   OPTIONAL,
+    freeFormatData-2                     [34] FreeFormatData                               OPTIONAL,
+    freeFormatDataAppend-2               [35] BOOLEAN                                      OPTIONAL,
+    partialRecordType                    [42] PartialRecordType                            OPTIONAL,
+    basicService                        [130] BasicServiceCode                             OPTIONAL,
+    additionalChgInfo                   [133] AdditionalChgInfo                            OPTIONAL,
+    chargedParty                        [141] ChargedParty                                 OPTIONAL,
+    originalCalledNumber                [142] OriginalCalledNumber                         OPTIONAL,
+    orgMSCId                            [168] MSCId                                        OPTIONAL,
+    subscriberCategory                  [190] SubscriberCategory                           OPTIONAL,
+    hotBillingTag                       [200] HotBillingTag                                OPTIONAL
+}
+
+IncGatewayRecord        ::= SET
+{
+    recordType                            [0] CallEventRecordType                          OPTIONAL,
+    callingNumber                         [1] CallingNumber                                OPTIONAL,
+    calledNumber                          [2] CalledNumber                                 OPTIONAL,
+    recordingEntity                       [3] RecordingEntity                              OPTIONAL,
+    mscIncomingROUTE                      [4] ROUTE                                        OPTIONAL,
+    mscOutgoingROUTE                      [5] ROUTE                                        OPTIONAL,
+    seizureTime                           [6] TimeStamp                                    OPTIONAL,
+    answerTime                            [7] TimeStamp                                    OPTIONAL,
+    releaseTime                           [8] TimeStamp                                    OPTIONAL,
+    callDuration                          [9] CallDuration                                 OPTIONAL,
+    causeForTerm                         [11] CauseForTerm                                 OPTIONAL,
+    diagnostics                          [12] Diagnostics                                  OPTIONAL,
+    callReference                        [13] CallReference                                OPTIONAL,
+    sequenceNumber                       [14] SequenceNumber                               OPTIONAL,
+    recordExtensions                     [15] ManagementExtensions                         OPTIONAL,
+    partialRecordType                    [22] PartialRecordType                            OPTIONAL,
+    iSDN-BC                              [23] ISDN-BC                                      OPTIONAL,
+    lLC                                  [24] LLC                                          OPTIONAL,
+    hLC                                  [25] HLC                                          OPTIONAL,
+    basicService                        [130] BasicServiceCode                             OPTIONAL,
+    additionalChgInfo                   [133] AdditionalChgInfo                            OPTIONAL,
+    chargedParty                        [141] ChargedParty                                 OPTIONAL,
+    originalCalledNumber                [142] OriginalCalledNumber                         OPTIONAL,
+    rateIndication                      [159] RateIndication                               OPTIONAL,
+    roamingNumber                       [160] RoamingNumber                                OPTIONAL,
+    mscIncomingCircuit                  [167] MSCCIC                                       OPTIONAL,
+    orgMSCId                            [168] MSCId                                        OPTIONAL,
+    callEmlppPriority                   [170] EmlppPriority                                OPTIONAL,
+    eaSubscriberInfo                    [174] EASubscriberInfo                             OPTIONAL,
+    selectedCIC                         [175] SelectedCIC                                  OPTIONAL,
+    cUGOutgoingAccessIndicator          [195] CUGOutgoingAccessIndicator                   OPTIONAL,
+    cUGInterlockCode                    [196] CUGInterlockCode                             OPTIONAL,
+    cUGIncomingAccessUsed               [197] CUGIncomingAccessUsed                        OPTIONAL,
+    mscIncomingRouteAttribute           [198] RouteAttribute                               OPTIONAL,
+    mscOutgoingRouteAttribute           [199] RouteAttribute                               OPTIONAL,
+    networkCallReference                [200] NetworkCallReference                         OPTIONAL,
+    setupTime                           [201] TimeStamp                                    OPTIONAL,
+    alertingTime                        [202] TimeStamp                                    OPTIONAL,
+    voiceIndicator                      [203] VoiceIndicator                               OPTIONAL,
+    bCategory                           [204] BCategory                                    OPTIONAL,
+    callType                            [205] CallType                                     OPTIONAL
+}
+
+OutGatewayRecord        ::= SET
+{
+    recordType                            [0] CallEventRecordType                          OPTIONAL,
+    callingNumber                         [1] CallingNumber                                OPTIONAL,
+    calledNumber                          [2] CalledNumber                                 OPTIONAL,
+    recordingEntity                       [3] RecordingEntity                              OPTIONAL,
+    mscIncomingROUTE                      [4] ROUTE                                        OPTIONAL,
+    mscOutgoingROUTE                      [5] ROUTE                                        OPTIONAL,
+    seizureTime                           [6] TimeStamp                                    OPTIONAL,
+    answerTime                            [7] TimeStamp                                    OPTIONAL,
+    releaseTime                           [8] TimeStamp                                    OPTIONAL,
+    callDuration                          [9] CallDuration                                 OPTIONAL,
+    causeForTerm                         [11] CauseForTerm                                 OPTIONAL,
+    diagnostics                          [12] Diagnostics                                  OPTIONAL,
+    callReference                        [13] CallReference                                OPTIONAL,
+    sequenceNumber                       [14] SequenceNumber                               OPTIONAL,
+    recordExtensions                     [15] ManagementExtensions                         OPTIONAL,
+    partialRecordType                    [22] PartialRecordType                            OPTIONAL,
+    basicService                        [130] BasicServiceCode                             OPTIONAL,
+    additionalChgInfo                   [133] AdditionalChgInfo                            OPTIONAL,
+    chargedParty                        [141] ChargedParty                                 OPTIONAL,
+    originalCalledNumber                [142] OriginalCalledNumber                         OPTIONAL,
+    rateIndication                      [159] RateIndication                               OPTIONAL,
+    roamingNumber                       [160] RoamingNumber                                OPTIONAL,
+    mscOutgoingCircuit                  [166] MSCCIC                                       OPTIONAL,
+    orgMSCId                            [168] MSCId                                        OPTIONAL,
+    eaSubscriberInfo                    [174] EASubscriberInfo                             OPTIONAL,
+    selectedCIC                         [175] SelectedCIC                                  OPTIONAL,
+    callEmlppPriority                   [170] EmlppPriority                                OPTIONAL,
+    cUGOutgoingAccessIndicator          [195] CUGOutgoingAccessIndicator                   OPTIONAL,
+    cUGInterlockCode                    [196] CUGInterlockCode                             OPTIONAL,
+    cUGIncomingAccessUsed               [197] CUGIncomingAccessUsed                        OPTIONAL,
+    mscIncomingRouteAttribute           [198] RouteAttribute                               OPTIONAL,
+    mscOutgoingRouteAttribute           [199] RouteAttribute                               OPTIONAL,
+    networkCallReference                [200] NetworkCallReference                         OPTIONAL,
+    setupTime                           [201] TimeStamp                                    OPTIONAL,
+    alertingTime                        [202] TimeStamp                                    OPTIONAL,
+    voiceIndicator                      [203] VoiceIndicator                               OPTIONAL,
+    bCategory                           [204] BCategory                                    OPTIONAL,
+    callType                            [205] CallType                                     OPTIONAL
+}
+
+TransitCallRecord        ::= SET
+{
+    recordType                            [0] CallEventRecordType                          OPTIONAL,
+    recordingEntity                       [1] RecordingEntity                              OPTIONAL,
+    mscIncomingROUTE                      [2] ROUTE                                        OPTIONAL,
+    mscOutgoingROUTE                      [3] ROUTE                                        OPTIONAL,
+    callingNumber                         [4] CallingNumber                                OPTIONAL,
+    calledNumber                          [5] CalledNumber                                 OPTIONAL,
+    isdnBasicService                      [6] BasicService                                 OPTIONAL,
+    seizureTime                           [7] TimeStamp                                    OPTIONAL,
+    answerTime                            [8] TimeStamp                                    OPTIONAL,
+    releaseTime                           [9] TimeStamp                                    OPTIONAL,
+    callDuration                         [10] CallDuration                                 OPTIONAL,
+    causeForTerm                         [12] CauseForTerm                                 OPTIONAL,
+    diagnostics                          [13] Diagnostics                                  OPTIONAL,
+    callReference                        [14] CallReference                                OPTIONAL,
+    sequenceNumber                       [15] SequenceNumber                               OPTIONAL,
+    recordExtensions                     [16] ManagementExtensions                         OPTIONAL,
+    partialRecordType                    [23] PartialRecordType                            OPTIONAL,
+    basicService                        [130] BasicServiceCode                             OPTIONAL,
+    additionalChgInfo                   [133] AdditionalChgInfo                            OPTIONAL,
+    originalCalledNumber                [142] OriginalCalledNumber                         OPTIONAL,
+    rateIndication                      [159] RateIndication                               OPTIONAL,
+    mscOutgoingCircuit                  [166] MSCCIC                                       OPTIONAL,
+    mscIncomingCircuit                  [167] MSCCIC                                       OPTIONAL,
+    orgMSCId                            [168] MSCId                                        OPTIONAL,
+    callEmlppPriority                   [170] EmlppPriority                                OPTIONAL,
+    eaSubscriberInfo                    [174] EASubscriberInfo                             OPTIONAL,
+    selectedCIC                         [175] SelectedCIC                                  OPTIONAL,
+    cUGOutgoingAccessIndicator          [195] CUGOutgoingAccessIndicator                   OPTIONAL,
+    cUGInterlockCode                    [196] CUGInterlockCode                             OPTIONAL,
+    cUGIncomingAccessUsed               [197] CUGIncomingAccessUsed                        OPTIONAL,
+    mscIncomingRouteAttribute           [198] RouteAttribute                               OPTIONAL,
+    mscOutgoingRouteAttribute           [199] RouteAttribute                               OPTIONAL,
+    networkCallReference                [200] NetworkCallReference                         OPTIONAL,
+    setupTime                           [201] TimeStamp                                    OPTIONAL,
+    alertingTime                        [202] TimeStamp                                    OPTIONAL,
+    voiceIndicator                      [203] VoiceIndicator                               OPTIONAL,
+    bCategory                           [204] BCategory                                    OPTIONAL,
+    callType                            [205] CallType                                     OPTIONAL
+}
+
+MOSMSRecord                ::= SET
+{
+    recordType                                 [0] CallEventRecordType                     OPTIONAL,
+    servedIMSI                                 [1] IMSI                                    OPTIONAL,
+    servedIMEI                                 [2] IMEI                                    OPTIONAL,
+    servedMSISDN                               [3] MSISDN                                  OPTIONAL,
+    msClassmark                                [4] Classmark                               OPTIONAL,
+    serviceCentre                              [5] AddressString                           OPTIONAL,
+    recordingEntity                            [6] RecordingEntity                         OPTIONAL,
+    location                                   [7] LocationAreaAndCell                     OPTIONAL,
+    messageReference                           [8] MessageReference                        OPTIONAL,
+    originationTime                            [9] TimeStamp                               OPTIONAL,
+    smsResult                                 [10] SMSResult                               OPTIONAL,
+    recordExtensions                          [11] ManagementExtensions                    OPTIONAL,
+    destinationNumber                         [12] SmsTpDestinationNumber                  OPTIONAL,
+    cAMELSMSInformation                       [13] CAMELSMSInformation                     OPTIONAL,
+    systemType                                [14] SystemType                              OPTIONAL,
+    basicService                             [130] BasicServiceCode                        OPTIONAL,
+    additionalChgInfo                        [133] AdditionalChgInfo                       OPTIONAL,
+    classmark3                               [140] Classmark3                              OPTIONAL,
+    chargedParty                             [141] ChargedParty                            OPTIONAL,
+    orgRNCorBSCId                            [167] RNCorBSCId                              OPTIONAL,
+    orgMSCId                                 [168] MSCId                                   OPTIONAL,
+    globalAreaID                             [188] GAI                                     OPTIONAL,
+    subscriberCategory                       [190] SubscriberCategory                      OPTIONAL,
+    firstmccmnc                              [192] MCCMNC                                  OPTIONAL,
+    smsUserDataType                          [195] SmsUserDataType                         OPTIONAL,
+    smstext                                  [196] SMSTEXT                                 OPTIONAL,
+    maximumNumberOfSMSInTheConcatenatedSMS   [197] MaximumNumberOfSMSInTheConcatenatedSMS  OPTIONAL,
+    concatenatedSMSReferenceNumber           [198] ConcatenatedSMSReferenceNumber          OPTIONAL,
+    sequenceNumberOfTheCurrentSMS            [199] SequenceNumberOfTheCurrentSMS           OPTIONAL,
+    hotBillingTag                            [200] HotBillingTag                           OPTIONAL,
+    callReference                            [201] CallReference                           OPTIONAL
+}
+
+MTSMSRecord                ::= SET
+{
+    recordType                                [0] CallEventRecordType                      OPTIONAL,
+    serviceCentre                             [1] AddressString                            OPTIONAL,
+    servedIMSI                                [2] IMSI                                     OPTIONAL,
+    servedIMEI                                [3] IMEI                                     OPTIONAL,
+    servedMSISDN                              [4] MSISDN                                   OPTIONAL,
+    msClassmark                               [5] Classmark                                OPTIONAL,
+    recordingEntity                           [6] RecordingEntity                          OPTIONAL,
+    location                                  [7] LocationAreaAndCell                      OPTIONAL,
+    deliveryTime                              [8] TimeStamp                                OPTIONAL,
+    smsResult                                 [9] SMSResult                                OPTIONAL,
+    recordExtensions                         [10] ManagementExtensions                     OPTIONAL,
+    systemType                               [11] SystemType                               OPTIONAL,
+    cAMELSMSInformation                      [12] CAMELSMSInformation                      OPTIONAL,
+    basicService                            [130] BasicServiceCode                         OPTIONAL,
+    additionalChgInfo                       [133] AdditionalChgInfo                        OPTIONAL,
+    classmark3                              [140] Classmark3                               OPTIONAL,
+    chargedParty                            [141] ChargedParty                             OPTIONAL,
+    orgRNCorBSCId                           [167] RNCorBSCId                               OPTIONAL,
+    orgMSCId                                [168] MSCId                                    OPTIONAL,
+    globalAreaID                            [188] GAI                                      OPTIONAL,
+    subscriberCategory                      [190] SubscriberCategory                       OPTIONAL,
+    firstmccmnc                             [192] MCCMNC                                   OPTIONAL,
+    smsUserDataType                         [195] SmsUserDataType                          OPTIONAL,
+    smstext                                 [196] SMSTEXT                                  OPTIONAL,
+    maximumNumberOfSMSInTheConcatenatedSMS  [197] MaximumNumberOfSMSInTheConcatenatedSMS   OPTIONAL,
+    concatenatedSMSReferenceNumber          [198] ConcatenatedSMSReferenceNumber           OPTIONAL,
+    sequenceNumberOfTheCurrentSMS           [199] SequenceNumberOfTheCurrentSMS            OPTIONAL,
+    hotBillingTag                           [200] HotBillingTag                            OPTIONAL,
+    origination                             [201] CallingNumber                            OPTIONAL,
+    callReference                           [202] CallReference                            OPTIONAL
+}
+
+HLRIntRecord            ::= SET
+{
+    recordType                             [0] CallEventRecordType                         OPTIONAL,
+    servedIMSI                             [1] IMSI                                        OPTIONAL,
+    servedMSISDN                           [2] MSISDN                                      OPTIONAL,
+    recordingEntity                        [3] RecordingEntity                             OPTIONAL,
+    basicService                           [4] BasicServiceCode                            OPTIONAL,
+    routingNumber                          [5] RoutingNumber                               OPTIONAL,
+    interrogationTime                      [6] TimeStamp                                   OPTIONAL,
+    numberOfForwarding                     [7] NumberOfForwarding                          OPTIONAL,
+    interrogationResult                    [8] HLRIntResult                                OPTIONAL,
+    recordExtensions                       [9] ManagementExtensions                        OPTIONAL,
+    orgMSCId                             [168] MSCId                                       OPTIONAL,
+    callReference                        [169] CallReference                               OPTIONAL
+}
+
+SSActionRecord            ::= SET
+{
+    recordType                             [0] CallEventRecordType                         OPTIONAL,
+    servedIMSI                             [1] IMSI                                        OPTIONAL,
+    servedIMEI                             [2] IMEI                                        OPTIONAL,
+    servedMSISDN                           [3] MSISDN                                      OPTIONAL,
+    msClassmark                            [4] Classmark                                   OPTIONAL,
+    recordingEntity                        [5] RecordingEntity                             OPTIONAL,
+    location                               [6] LocationAreaAndCell                         OPTIONAL,
+    basicServices                          [7] BasicServices                               OPTIONAL,
+    supplService                           [8] SS-Code                                     OPTIONAL,
+    ssAction                               [9] SSActionType                                OPTIONAL,
+    ssActionTime                          [10] TimeStamp                                   OPTIONAL,
+    ssParameters                          [11] SSParameters                                OPTIONAL,
+    ssActionResult                        [12] SSActionResult                              OPTIONAL,
+    callReference                         [13] CallReference                               OPTIONAL,
+    recordExtensions                      [14] ManagementExtensions                        OPTIONAL,
+    systemType                            [15] SystemType                                  OPTIONAL,
+    ussdCodingScheme                     [126] UssdCodingScheme                            OPTIONAL,
+    ussdString                           [127] SEQUENCE OF UssdString                      OPTIONAL,
+    ussdNotifyCounter                    [128] UssdNotifyCounter                           OPTIONAL,
+    ussdRequestCounter                   [129] UssdRequestCounter                          OPTIONAL,
+    additionalChgInfo                    [133] AdditionalChgInfo                           OPTIONAL,
+    classmark3                           [140] Classmark3                                  OPTIONAL,
+    chargedParty                         [141] ChargedParty                                OPTIONAL,
+    orgRNCorBSCId                        [167] RNCorBSCId                                  OPTIONAL,
+    orgMSCId                             [168] MSCId                                       OPTIONAL,
+    globalAreaID                         [188] GAI                                         OPTIONAL,
+    subscriberCategory                   [190] SubscriberCategory                          OPTIONAL,
+    firstmccmnc                          [192] MCCMNC                                      OPTIONAL,
+    hotBillingTag                        [200] HotBillingTag                               OPTIONAL
+}
+
+CommonEquipRecord         ::= SET
+{
+    recordType                         [0] CallEventRecordType                             OPTIONAL,
+    equipmentType                      [1] EquipmentType                                   OPTIONAL,
+    equipmentId                        [2] EquipmentId                                     OPTIONAL,
+    servedIMSI                         [3] IMSI                                            OPTIONAL,
+    servedMSISDN                       [4] MSISDN                                          OPTIONAL,
+    recordingEntity                    [5] RecordingEntity                                 OPTIONAL,
+    basicService                       [6] BasicServiceCode                                OPTIONAL,
+    changeOfService                    [7] SEQUENCE OF ChangeOfService                     OPTIONAL,
+    supplServicesUsed                  [8] SEQUENCE OF SuppServiceUsed                     OPTIONAL,
+    seizureTime                        [9] TimeStamp                                       OPTIONAL,
+    releaseTime                       [10] TimeStamp                                       OPTIONAL,
+    callDuration                      [11] CallDuration                                    OPTIONAL,
+    callReference                     [12] CallReference                                   OPTIONAL,
+    sequenceNumber                    [13] SequenceNumber                                  OPTIONAL,
+    recordExtensions                  [14] ManagementExtensions                            OPTIONAL,
+    systemType                        [15] SystemType                                      OPTIONAL,
+    rateIndication                    [16] RateIndication                                  OPTIONAL,
+    fnur                              [17] Fnur                                            OPTIONAL,
+    partialRecordType                 [18] PartialRecordType                               OPTIONAL,
+    causeForTerm                     [100] CauseForTerm                                    OPTIONAL,
+    diagnostics                      [101] Diagnostics                                     OPTIONAL,
+    servedIMEI                       [102] IMEI                                            OPTIONAL,
+    additionalChgInfo                [133] AdditionalChgInfo                               OPTIONAL,
+    orgRNCorBSCId                    [167] RNCorBSCId                                      OPTIONAL,
+    orgMSCId                         [168] MSCId                                           OPTIONAL,
+    subscriberCategory               [190] SubscriberCategory                              OPTIONAL,
+    hotBillingTag                    [200] HotBillingTag                                   OPTIONAL
+}
+
+------------------------------------------------------------------------------
+--
+--  OBSERVED IMEI TICKETS
+--
+------------------------------------------------------------------------------
+
+ObservedIMEITicket              ::= SET
+{
+    servedIMEI                        [0] IMEI,
+    imeiStatus                        [1] IMEIStatus,
+    servedIMSI                        [2] IMSI,
+    servedMSISDN                      [3] MSISDN                       OPTIONAL,
+    recordingEntity                   [4] RecordingEntity,
+    eventTime                         [5] TimeStamp,
+    location                          [6] LocationAreaAndCell,
+    imeiCheckEvent                    [7] IMEICheckEvent               OPTIONAL,
+    callReference                     [8] CallReference                OPTIONAL,
+    recordExtensions                  [9] ManagementExtensions         OPTIONAL,
+    orgMSCId                        [168] MSCId                        OPTIONAL
+}
+
+
+
+------------------------------------------------------------------------------
+--
+--  LOCATION SERICE TICKETS
+--
+------------------------------------------------------------------------------
+
+MTLCSRecord                ::= SET
+{
+    recordType                            [0] CallEventRecordType                 OPTIONAL,
+    recordingEntity                       [1] RecordingEntity                     OPTIONAL,
+    lcsClientType                         [2] LCSClientType                       OPTIONAL,
+    lcsClientIdentity                     [3] LCSClientIdentity                   OPTIONAL,
+    servedIMSI                            [4] IMSI                                OPTIONAL,
+    servedMSISDN                          [5] MSISDN                              OPTIONAL,
+    locationType                          [6] LocationType                        OPTIONAL,
+    lcsQos                                [7] LCSQoSInfo                          OPTIONAL,
+    lcsPriority                           [8] LCS-Priority                        OPTIONAL,
+    mlc-Number                            [9] ISDN-AddressString                  OPTIONAL,
+    eventTimeStamp                       [10] TimeStamp                           OPTIONAL,
+    measureDuration                      [11] CallDuration                        OPTIONAL,
+    notificationToMSUser                 [12] NotificationToMSUser                OPTIONAL,
+    privacyOverride                      [13] NULL                                OPTIONAL,
+    location                             [14] LocationAreaAndCell                 OPTIONAL,
+    locationEstimate                     [15] Ext-GeographicalInformation         OPTIONAL,
+    positioningData                      [16] PositioningData                     OPTIONAL,
+    lcsCause                             [17] LCSCause                            OPTIONAL,
+    diagnostics                          [18] Diagnostics                         OPTIONAL,
+    systemType                           [19] SystemType                          OPTIONAL,
+    recordExtensions                     [20] ManagementExtensions                OPTIONAL,
+    causeForTerm                         [21] CauseForTerm                        OPTIONAL,
+    lcsReferenceNumber                  [101] CallReferenceNumber                 OPTIONAL,
+    servedIMEI                          [102] IMEI                                OPTIONAL,
+    additionalChgInfo                   [133] AdditionalChgInfo                   OPTIONAL,
+    chargedParty                        [141] ChargedParty                        OPTIONAL,
+    orgRNCorBSCId                       [167] RNCorBSCId                          OPTIONAL,
+    orgMSCId                            [168] MSCId                               OPTIONAL,
+    globalAreaID                        [188] GAI                                 OPTIONAL,
+    subscriberCategory                  [190] SubscriberCategory                  OPTIONAL,
+    firstmccmnc                         [192] MCCMNC                              OPTIONAL,
+    hotBillingTag                       [200] HotBillingTag                       OPTIONAL,
+    callReference                       [201] CallReference                       OPTIONAL
+}
+
+MOLCSRecord                ::= SET
+{
+     recordType                         [0] CallEventRecordType                   OPTIONAL,
+     recordingEntity                    [1] RecordingEntity                       OPTIONAL,
+     lcsClientType                      [2] LCSClientType                         OPTIONAL,
+     lcsClientIdentity                  [3] LCSClientIdentity                     OPTIONAL,
+     servedIMSI                         [4] IMSI                                  OPTIONAL,
+     servedMSISDN                       [5] MSISDN                                OPTIONAL,
+     molr-Type                          [6] MOLR-Type                             OPTIONAL,
+     lcsQos                             [7] LCSQoSInfo                            OPTIONAL,
+     lcsPriority                        [8] LCS-Priority                          OPTIONAL,
+     mlc-Number                         [9] ISDN-AddressString                    OPTIONAL,
+     eventTimeStamp                    [10] TimeStamp                             OPTIONAL,
+     measureDuration                   [11] CallDuration                          OPTIONAL,
+     location                          [12] LocationAreaAndCell                   OPTIONAL,
+     locationEstimate                  [13] Ext-GeographicalInformation           OPTIONAL,
+     positioningData                   [14] PositioningData                       OPTIONAL,
+     lcsCause                          [15] LCSCause                              OPTIONAL,
+     diagnostics                       [16] Diagnostics                           OPTIONAL,
+     systemType                        [17] SystemType                            OPTIONAL,
+     recordExtensions                  [18] ManagementExtensions                  OPTIONAL,
+     causeForTerm                      [19] CauseForTerm                          OPTIONAL,
+     lcsReferenceNumber               [101] CallReferenceNumber                   OPTIONAL,
+     servedIMEI                       [102] IMEI                                  OPTIONAL,
+     additionalChgInfo                [133] AdditionalChgInfo                     OPTIONAL,
+     chargedParty                     [141] ChargedParty                          OPTIONAL,
+     orgRNCorBSCId                    [167] RNCorBSCId                            OPTIONAL,
+     orgMSCId                         [168] MSCId                                 OPTIONAL,
+     globalAreaID                     [188] GAI                                   OPTIONAL,
+     subscriberCategory               [190] SubscriberCategory                    OPTIONAL,
+     firstmccmnc                      [192] MCCMNC                                OPTIONAL,
+     hotBillingTag                    [200] HotBillingTag                         OPTIONAL,
+    callReference                     [201] CallReference                         OPTIONAL
+}
+
+NILCSRecord                ::= SET
+{
+    recordType                        [0] CallEventRecordType                     OPTIONAL,
+    recordingEntity                   [1] RecordingEntity                         OPTIONAL,
+    lcsClientType                     [2] LCSClientType                           OPTIONAL,
+    lcsClientIdentity                 [3] LCSClientIdentity                       OPTIONAL,
+    servedIMSI                        [4] IMSI                                    OPTIONAL,
+    servedMSISDN                      [5] MSISDN                                  OPTIONAL,
+    servedIMEI                        [6] IMEI                                    OPTIONAL,
+    emsDigits                         [7] ISDN-AddressString                      OPTIONAL,
+    emsKey                            [8] ISDN-AddressString                      OPTIONAL,
+    lcsQos                            [9] LCSQoSInfo                              OPTIONAL,
+    lcsPriority                      [10] LCS-Priority                            OPTIONAL,
+    mlc-Number                       [11] ISDN-AddressString                      OPTIONAL,
+    eventTimeStamp                   [12] TimeStamp                               OPTIONAL,
+    measureDuration                  [13] CallDuration                            OPTIONAL,
+    location                         [14] LocationAreaAndCell                     OPTIONAL,
+    locationEstimate                 [15] Ext-GeographicalInformation             OPTIONAL,
+    positioningData                  [16] PositioningData                         OPTIONAL,
+    lcsCause                         [17] LCSCause                                OPTIONAL,
+    diagnostics                      [18] Diagnostics                             OPTIONAL,
+    systemType                       [19] SystemType                              OPTIONAL,
+    recordExtensions                 [20] ManagementExtensions                    OPTIONAL,
+    causeForTerm                     [21] CauseForTerm                            OPTIONAL,
+    lcsReferenceNumber              [101] CallReferenceNumber                     OPTIONAL,
+    additionalChgInfo               [133] AdditionalChgInfo                       OPTIONAL,
+    chargedParty                    [141] ChargedParty                            OPTIONAL,
+    orgRNCorBSCId                   [167] RNCorBSCId                              OPTIONAL,
+    orgMSCId                        [168] MSCId                                   OPTIONAL,
+    globalAreaID                    [188] GAI                                     OPTIONAL,
+    subscriberCategory              [190] SubscriberCategory                      OPTIONAL,
+    firstmccmnc                     [192] MCCMNC                                  OPTIONAL,
+    hotBillingTag                   [200] HotBillingTag                           OPTIONAL,
+    callReference                   [201] CallReference                           OPTIONAL
+}
+
+
+------------------------------------------------------------------------------
+--
+--  FTAM / FTP / TFTP FILE CONTENTS
+--
+------------------------------------------------------------------------------
+
+CallEventDataFile        ::= SEQUENCE
+{
+    headerRecord            [0] HeaderRecord,
+    callEventRecords        [1] SEQUENCE OF CallEventRecord,
+    trailerRecord           [2] TrailerRecord,
+    extensions              [3] ManagementExtensions
+}
+
+ObservedIMEITicketFile    ::= SEQUENCE
+{
+    productionDateTime      [0] TimeStamp,
+    observedIMEITickets     [1] SEQUENCE OF ObservedIMEITicket,
+    noOfRecords             [2] INTEGER,
+    extensions              [3] ManagementExtensions
+}
+
+HeaderRecord            ::= SEQUENCE
+{
+    productionDateTime      [0] TimeStamp,
+    recordingEntity         [1] RecordingEntity,
+    extensions              [2] ManagementExtensions
+}
+
+TrailerRecord            ::= SEQUENCE
+{
+    productionDateTime      [0] TimeStamp,
+    recordingEntity         [1] RecordingEntity,
+    firstCallDateTime       [2] TimeStamp,
+    lastCallDateTime        [3] TimeStamp,
+    noOfRecords             [4] INTEGER,
+    extensions              [5] ManagementExtensions
+}
+
+
+------------------------------------------------------------------------------
+--
+--  COMMON DATA TYPES
+--
+------------------------------------------------------------------------------
+
+AdditionalChgInfo        ::= SEQUENCE
+{
+    chargeIndicator     [0] ChargeIndicator      OPTIONAL,
+    chargeParameters    [1] OCTET STRING         OPTIONAL
+}
+
+AddressString ::= OCTET STRING -- (SIZE (1..maxAddressLength))
+    -- This type is used to represent a number for addressing
+    -- purposes. It is composed of
+    --    a)    one octet for nature of address, and numbering plan
+    --        indicator.
+    --    b)    digits of an address encoded as TBCD-String.
+
+    -- a)    The first octet includes a one bit extension indicator, a
+    --        3 bits nature of address indicator and a 4 bits numbering
+    --        plan indicator, encoded as follows:
+
+    -- bit 8: 1  (no extension)
+
+    -- bits 765: nature of address indicator
+    --    000  unknown
+    --    001  international number
+    --    010  national significant number
+    --    011  network specific number
+    --    100  subscriber number
+    --    101  reserved
+    --    110  abbreviated number
+    --    111  reserved for extension
+
+    -- bits 4321: numbering plan indicator
+    --    0000  unknown
+    --    0001  ISDN/Telephony Numbering Plan (Rec CCITT E.164)
+    --    0010  spare
+    --    0011  data numbering plan (CCITT Rec X.121)
+    --    0100  telex numbering plan (CCITT Rec F.69)
+    --    0101  spare
+    --    0110  land mobile numbering plan (CCITT Rec E.212)
+    --    0111  spare
+    --    1000  national numbering plan
+    --    1001  private numbering plan
+    --    1111  reserved for extension
+
+    --    all other values are reserved.
+
+    -- b)    The following octets representing digits of an address
+    --        encoded as a TBCD-STRING.
+
+-- maxAddressLength  INTEGER ::= 20
+
+AiurRequested            ::= ENUMERATED
+{
+    --
+    -- See Bearer Capability TS 24.008
+    -- (note that value "4" is intentionally missing
+    --  because it is not used in TS 24.008)
+    --
+
+    aiur09600BitsPerSecond        (1),
+    aiur14400BitsPerSecond        (2),
+    aiur19200BitsPerSecond        (3),
+    aiur28800BitsPerSecond        (5),
+    aiur38400BitsPerSecond        (6),
+    aiur43200BitsPerSecond        (7),
+    aiur57600BitsPerSecond        (8),
+    aiur38400BitsPerSecond1       (9),
+    aiur38400BitsPerSecond2       (10),
+    aiur38400BitsPerSecond3       (11),
+    aiur38400BitsPerSecond4       (12)
+}
+
+AOCParameters            ::= SEQUENCE
+{
+    --
+    -- See TS 22.024.
+    --
+    e1                    [1] EParameter      OPTIONAL,
+    e2                    [2] EParameter      OPTIONAL,
+    e3                    [3] EParameter      OPTIONAL,
+    e4                    [4] EParameter      OPTIONAL,
+    e5                    [5] EParameter      OPTIONAL,
+    e6                    [6] EParameter      OPTIONAL,
+    e7                    [7] EParameter      OPTIONAL
+}
+
+AOCParmChange            ::= SEQUENCE
+{
+    changeTime            [0] TimeStamp,
+    newParameters         [1] AOCParameters
+}
+
+BasicService                  ::= OCTET STRING -- (SIZE(1))
+
+--This parameter identifies the ISDN Basic service as defined in ETSI specification ETS 300 196.
+--     allServices                                      '00'h
+--     speech                                           '01'h
+--     unrestricteDigtalInfo                            '02'h
+--     audio3k1HZ                                       '03'h
+--     unrestricteDigtalInfowithtoneandannoucement      '04'h
+--     telephony3k1HZ                                   '20'h
+--     teletext                                         '21'h
+--     telefaxGroup4Class1                              '22'h
+--     videotextSyntaxBased                             '23'h
+--     videotelephony                                   '24'h
+--     telefaxGroup2-3                                  '25'h
+--     telephony7kHZ                                    '26'h
+
+
+
+BasicServices            ::= SET OF BasicServiceCode
+
+BasicServiceCode ::= CHOICE
+{
+    bearerService    [2] BearerServiceCode,
+    teleservice      [3] TeleserviceCode
+}
+
+
+TeleserviceCode ::= OCTET STRING -- (SIZE (1))
+    -- This type is used to represent the code identifying a single
+    -- teleservice, a group of teleservices, or all teleservices. The
+    -- services are defined in TS GSM 02.03.
+    -- The internal structure is defined as follows:
+
+    -- bits 87654321: group (bits 8765) and specific service
+    -- (bits 4321)
+
+--    allTeleservices                 (0x00),
+--    allSpeechTransmissionServices   (0x10),
+--    telephony                       (0x11),
+--    emergencyCalls                  (0x12),
+--
+--    allShortMessageServices         (0x20),
+--    shortMessageMT-PP               (0x21),
+--    shortMessageMO-PP               (0x22),
+--
+--    allFacsimileTransmissionServices (0x60),
+--    facsimileGroup3AndAlterSpeech    (0x61),
+--    automaticFacsimileGroup3         (0x62),
+--    facsimileGroup4                  (0x63),
+--
+--     The following non-hierarchical Compound Teleservice Groups
+--     are defined in TS GSM 02.30:
+--    allDataTeleservices              (0x70),
+--         covers Teleservice Groups 'allFacsimileTransmissionServices'
+--         and 'allShortMessageServices'
+--    allTeleservices-ExeptSMS         (0x80),
+--       covers Teleservice Groups 'allSpeechTransmissionServices' and
+--       'allFacsimileTransmissionServices'
+--
+--    Compound Teleservice Group Codes are only used in call
+--    independent supplementary service operations, i.e. they
+--    are not used in InsertSubscriberData or in
+--    DeleteSubscriberData messages.
+--
+--    allVoiceGroupCallServices (0x90),
+--    voiceGroupCall            (0x91),
+--    voiceBroadcastCall        (0x92),
+--
+--    allPLMN-specificTS        (0xd0),
+--    plmn-specificTS-1         (0xd1),
+--    plmn-specificTS-2         (0xd2),
+--    plmn-specificTS-3         (0xd3),
+--    plmn-specificTS-4         (0xd4),
+--    plmn-specificTS-5         (0xd5),
+--    plmn-specificTS-6         (0xd6),
+--    plmn-specificTS-7         (0xd7),
+--    plmn-specificTS-8         (0xd8),
+--    plmn-specificTS-9         (0xd9),
+--    plmn-specificTS-A         (0xda),
+--    plmn-specificTS-B         (0xdb),
+--    plmn-specificTS-C         (0xdc),
+--    plmn-specificTS-D         (0xdd),
+--    plmn-specificTS-E         (0xde),
+--    plmn-specificTS-F         (0xdf)
+
+
+BearerServiceCode ::= OCTET STRING -- (SIZE (1))
+    -- This type is used to represent the code identifying a single
+    -- bearer service, a group of bearer services, or all bearer
+    -- services. The services are defined in TS 3GPP TS 22.002 [3].
+    -- The internal structure is defined as follows:
+    --
+    -- plmn-specific bearer services:
+    -- bits 87654321: defined by the HPLMN operator
+
+    -- rest of bearer services:
+    -- bit 8: 0 (unused)
+    -- bits 7654321: group (bits 7654), and rate, if applicable
+    -- (bits 321)
+
+--    allBearerServices          (0x00),
+--    allDataCDA-Services        (0x10),
+--    dataCDA-300bps             (0x11),
+--    dataCDA-1200bps            (0x12),
+--    dataCDA-1200-75bps         (0x13),
+--    dataCDA-2400bps            (0x14),
+--    dataCDA-4800bps            (0x15),
+--    dataCDA-9600bps            (0x16),
+--    general-dataCDA            (0x17),
+--
+--    allDataCDS-Services        (0x18),
+--    dataCDS-1200bps            (0x1a),
+--    dataCDS-2400bps            (0x1c),
+--    dataCDS-4800bps            (0x1d),
+--    dataCDS-9600bps            (0x1e),
+--    general-dataCDS            (0x1f),
+--
+--    allPadAccessCA-Services      (0x20),
+--    padAccessCA-300bps           (0x21),
+--    padAccessCA-1200bps          (0x22),
+--    padAccessCA-1200-75bps       (0x23),
+--    padAccessCA-2400bps          (0x24),
+--    padAccessCA-4800bps          (0x25),
+--    padAccessCA-9600bps          (0x26),
+--    general-padAccessCA          (0x27),
+--
+--    allDataPDS-Services          (0x28),
+--    dataPDS-2400bps              (0x2c),
+--    dataPDS-4800bps              (0x2d),
+--    dataPDS-9600bps              (0x2e),
+--    general-dataPDS              (0x2f),
+--
+--    allAlternateSpeech-DataCDA            (0x30),
+--
+--    allAlternateSpeech-DataCDS            (0x38),
+--
+--    allSpeechFollowedByDataCDA            (0x40),
+--
+--    allSpeechFollowedByDataCDS            (0x48),
+--
+--     The following non-hierarchical Compound Bearer Service
+--     Groups are defined in TS GSM 02.30:
+--    allDataCircuitAsynchronous              (0x50),
+--         covers "allDataCDA-Services", "allAlternateSpeech-DataCDA" and
+--         "allSpeechFollowedByDataCDA"
+--    allDataCircuitSynchronous               (0x58),
+--         covers "allDataCDS-Services", "allAlternateSpeech-DataCDS" and
+--         "allSpeechFollowedByDataCDS"
+--    allAsynchronousServices                 (0x60),
+--         covers "allDataCDA-Services", "allAlternateSpeech-DataCDA",
+--         "allSpeechFollowedByDataCDA" and "allPadAccessCDA-Services"
+--    allSynchronousServices                  (0x68),
+--        covers "allDataCDS-Services", "allAlternateSpeech-DataCDS",
+--        "allSpeechFollowedByDataCDS" and "allDataPDS-Services"
+--
+--     Compound Bearer Service Group Codes are only used in call
+--     independent supplementary service operations, i.e. they
+--     are not used in InsertSubscriberData or in
+--     DeleteSubscriberData messages.
+--
+--    allPLMN-specificBS           (0xd0),
+--    plmn-specificBS-1            (0xd1),
+--    plmn-specificBS-2            (0xd2),
+--    plmn-specificBS-3            (0xd3),
+--    plmn-specificBS-4            (0xd4),
+--    plmn-specificBS-5            (0xd5),
+--    plmn-specificBS-6            (0xd6),
+--    plmn-specificBS-7            (0xd7),
+--    plmn-specificBS-8            (0xd8),
+--    plmn-specificBS-9            (0xd9),
+--    plmn-specificBS-A            (0xda),
+--    plmn-specificBS-B            (0xdb),
+--    plmn-specificBS-C            (0xdc),
+--    plmn-specificBS-D            (0xdd),
+--    plmn-specificBS-E            (0xde),
+--    plmn-specificBS-F            (0xdf)
+
+
+BCDDirectoryNumber        ::= OCTET STRING
+    -- This type contains the binary coded decimal representation of
+    -- a directory number e.g. calling/called/connected/translated number.
+    -- The encoding of the octet string is in accordance with the
+    -- the elements "Calling party BCD number", "Called party BCD number"
+    -- and "Connected number" defined in TS 24.008.
+    -- This encoding includes type of number and number plan information
+    -- together with a BCD encoded digit string.
+    -- It may also contain both a presentation and screening indicator
+    -- (octet 3a).
+    -- For the avoidance of doubt, this field does not include
+    -- octets 1 and 2, the element name and length, as this would be
+    -- redundant.
+
+CallDuration             ::= INTEGER
+    --
+    -- The call duration in seconds.
+    -- For successful calls this is the chargeable duration.
+    -- For call attempts this is the call holding time.
+    --
+
+CallEventRecordType     ::= ENUMERATED -- INTEGER
+{
+    moCallRecord          (0),
+    mtCallRecord          (1),
+    roamingRecord         (2),
+    incGatewayRecord      (3),
+    outGatewayRecord      (4),
+    transitCallRecord     (5),
+    moSMSRecord           (6),
+    mtSMSRecord           (7),
+    ssActionRecord        (10),
+    hlrIntRecord          (11),
+    commonEquipRecord     (14),
+    moTraceRecord         (15),
+    mtTraceRecord         (16),
+    termCAMELRecord       (17),
+    mtLCSRecord           (23),
+    moLCSRecord           (24),
+    niLCSRecord           (25),
+    forwardCallRecord     (100)
+}
+
+CalledNumber             ::= BCDDirectoryNumber
+
+CallingNumber            ::= BCDDirectoryNumber
+
+CallingPartyCategory     ::= Category
+
+CallReference            ::= OCTET STRING -- (SIZE (1..8))
+
+CallReferenceNumber ::= OCTET STRING -- (SIZE (1..8))
+
+CAMELDestinationNumber    ::= DestinationRoutingAddress
+
+CAMELInformation        ::= SET
+{
+    cAMELDestinationNumber      [1] CAMELDestinationNumber       OPTIONAL,
+    connectedNumber             [2] ConnectedNumber                  OPTIONAL,
+    roamingNumber               [3] RoamingNumber                OPTIONAL,
+    mscOutgoingROUTE            [4] ROUTE                        OPTIONAL,
+    seizureTime                 [5] TimeStamp                    OPTIONAL,
+    answerTime                  [6] TimeStamp                    OPTIONAL,
+    releaseTime                 [7] TimeStamp                    OPTIONAL,
+    callDuration                [8] CallDuration                 OPTIONAL,
+    dataVolume                  [9] DataVolume                   OPTIONAL,
+    cAMELInitCFIndicator       [10] CAMELInitCFIndicator         OPTIONAL,
+    causeForTerm               [11] CauseForTerm                 OPTIONAL,
+    cAMELModification          [12] ChangedParameters            OPTIONAL,
+    freeFormatData             [13] FreeFormatData               OPTIONAL,
+    diagnostics                [14] Diagnostics                  OPTIONAL,
+    freeFormatDataAppend       [15] BOOLEAN                      OPTIONAL,
+    freeFormatData-2           [16] FreeFormatData               OPTIONAL,
+    freeFormatDataAppend-2     [17] BOOLEAN                      OPTIONAL
+}
+
+CAMELSMSInformation        ::= SET
+{
+    gsm-SCFAddress                [1] Gsm-SCFAddress             OPTIONAL,
+    serviceKey                    [2] ServiceKey                 OPTIONAL,
+    defaultSMSHandling            [3] DefaultSMS-Handling        OPTIONAL,
+    freeFormatData                [4] FreeFormatData             OPTIONAL,
+    callingPartyNumber            [5] CallingNumber              OPTIONAL,
+    destinationSubscriberNumber   [6] CalledNumber               OPTIONAL,
+    cAMELSMSCAddress              [7] AddressString              OPTIONAL,
+    smsReferenceNumber            [8] CallReferenceNumber        OPTIONAL
+}
+
+CAMELInitCFIndicator    ::= ENUMERATED
+{
+    noCAMELCallForwarding      (0),
+    cAMELCallForwarding        (1)
+}
+
+CAMELModificationParameters    ::= SET
+    --
+    -- The list contains only parameters changed due to CAMEL call
+    -- handling.
+    --
+{
+    callingPartyNumber            [0] CallingNumber             OPTIONAL,
+    callingPartyCategory          [1] CallingPartyCategory      OPTIONAL,
+    originalCalledPartyNumber     [2] OriginalCalledNumber      OPTIONAL,
+    genericNumbers                [3] GenericNumbers            OPTIONAL,
+    redirectingPartyNumber        [4] RedirectingNumber         OPTIONAL,
+    redirectionCounter            [5] NumberOfForwarding        OPTIONAL
+}
+
+
+Category        ::= OCTET STRING -- (SIZE(1))
+    --
+    -- The internal structure is defined in ITU-T Rec Q.763.
+    --see subscribe category
+
+CauseForTerm            ::= ENUMERATED -- INTEGER
+    --
+    -- Cause codes from 16 up to 31 are defined in TS 32.015 as 'CauseForRecClosing'
+    -- (cause for record closing).
+    -- There is no direct correlation between these two types.
+    -- LCS related causes belong to the MAP error causes acc. TS 29.002.
+    --
+{
+    normalRelease                               (0),
+    partialRecord                               (1),
+    partialRecordCallReestablishment            (2),
+    unsuccessfulCallAttempt                     (3),
+    stableCallAbnormalTermination               (4),
+    cAMELInitCallRelease                        (5),
+    unauthorizedRequestingNetwork               (52),
+    unauthorizedLCSClient                       (53),
+    positionMethodFailure                       (54),
+    unknownOrUnreachableLCSClient               (58)
+}
+
+CellId    ::= OCTET STRING -- (SIZE(2))
+    --
+    -- Coded according to TS 24.008
+    --
+
+ChangedParameters        ::= SET
+{
+    changeFlags         [0] ChangeFlags,
+    changeList      [1] CAMELModificationParameters    OPTIONAL
+}
+
+ChangeFlags                ::= BIT STRING
+--     {
+--          callingPartyNumberModified            (0),
+--          callingPartyCategoryModified          (1),
+--          originalCalledPartyNumberModified     (2),
+--          genericNumbersModified                (3),
+--          redirectingPartyNumberModified        (4),
+--          redirectionCounterModified            (5)
+--     }
+
+ChangeOfClassmark         ::= SEQUENCE
+{
+    classmark             [0] Classmark,
+    changeTime            [1] TimeStamp
+}
+
+ChangeOfRadioChannel     ::= SEQUENCE
+{
+    radioChannel         [0] TrafficChannel,
+    changeTime           [1] TimeStamp,
+    speechVersionUsed    [2] SpeechVersionIdentifier     OPTIONAL
+}
+
+ChangeOfService         ::= SEQUENCE
+{
+    basicService          [0] BasicServiceCode,
+    transparencyInd       [1] TransparencyInd      OPTIONAL,
+    changeTime            [2] TimeStamp,
+    rateIndication        [3] RateIndication       OPTIONAL,
+    fnur                  [4] Fnur OPTIONAL
+}
+
+ChannelCoding            ::= ENUMERATED
+{
+    tchF4800             (1),
+    tchF9600             (2),
+    tchF14400            (3)
+}
+
+ChargeIndicator            ::= ENUMERATED -- INTEGER
+{
+    noIndication        (0),
+    noCharge            (1),
+    charge              (2)
+}
+
+Classmark                ::= OCTET STRING
+    --
+    -- See Mobile station classmark  2 or 3  TS 24.008
+    --
+
+ConnectedNumber           ::= BCDDirectoryNumber
+
+DataVolume                ::= INTEGER
+    --
+    -- The volume of data transferred in segments of 64 octets.
+    --
+
+Day                       ::= INTEGER -- (1..31)
+
+--DayClass                ::= ObjectInstance
+
+--DayClasses              ::= SET OF DayClass
+
+--DayDefinition           ::= SEQUENCE
+--{
+--    day                 [0] DayOfTheWeek,
+--    dayClass            [1] ObjectInstance
+--}
+
+--DayDefinitions            ::= SET OF DayDefinition
+
+--DateDefinition            ::= SEQUENCE
+--{
+--    month                [0] Month,
+--    day                  [1] Day,
+--    dayClass             [2] ObjectInstance
+--}
+
+--DateDefinitions         ::= SET OF DateDefinition
+
+--DayOfTheWeek            ::= ENUMERATED
+--{
+--    allDays              (0),
+--    sunday               (1),
+--    monday               (2),
+--    tuesday              (3),
+--    wednesday            (4),
+--    thursday             (5),
+--    friday               (6),
+--    saturday             (7)
+--}
+
+DestinationRoutingAddress    ::= BCDDirectoryNumber
+
+DefaultCallHandling ::= ENUMERATED
+{
+    continueCall     (0),
+    releaseCall      (1)
+}
+    -- exception handling:
+    -- reception of values in range 2-31 shall be treated as "continueCall"
+    -- reception of values greater than 31 shall be treated as "releaseCall"
+
+DeferredLocationEventType ::= BIT STRING
+--     {
+--         msAvailable            (0)
+--     } (SIZE (1..16))
+
+    -- exception handling
+    -- a ProvideSubscriberLocation-Arg containing other values than listed above in
+    -- DeferredLocationEventType shall be rejected by the receiver with a return error cause of
+    -- unexpected data value.
+
+Diagnostics                        ::= CHOICE
+{
+    gsm0408Cause                [0] INTEGER,
+    -- See TS 24.008
+    gsm0902MapErrorValue        [1] INTEGER,
+    -- Note: The value to be stored here corresponds to
+    -- the local values defined in the MAP-Errors and
+    -- MAP-DialogueInformation modules, for full details
+    -- see TS 29.002.
+    ccittQ767Cause              [2] INTEGER,
+    -- See ITU-T Q.767
+    networkSpecificCause        [3] ManagementExtension,
+    -- To be defined by network operator
+    manufacturerSpecificCause   [4] ManagementExtension
+    -- To be defined by manufacturer
+}
+
+DefaultSMS-Handling ::= ENUMERATED
+{
+    continueTransaction             (0) ,
+    releaseTransaction              (1)
+}
+--    exception handling:
+--    reception of values in range 2-31 shall be treated as "continueTransaction"
+--    reception of values greater than 31 shall be treated as "releaseTransaction"
+
+--Destinations            ::= SET OF AE-title
+
+EmergencyCallIndEnable    ::= BOOLEAN
+
+EmergencyCallIndication    ::= SEQUENCE
+{
+    cellId                [0] CellId,
+    callerId              [1] IMSIorIMEI
+}
+
+EParameter    ::= INTEGER -- (0..1023)
+    --
+    -- Coded according to  TS 22.024  and TS 24.080
+    --
+
+EquipmentId                ::= INTEGER
+
+Ext-GeographicalInformation ::= OCTET STRING -- (SIZE (1..maxExt-GeographicalInformation))
+    -- Refers to geographical Information defined in 3G TS 23.032.
+    -- This is composed of 1 or more octets with an internal structure according to
+    -- 3G TS 23.032
+    -- Octet 1: Type of shape, only the following shapes in 3G TS 23.032 are allowed:
+    --        (a) Ellipsoid point with uncertainty circle
+    --        (b) Ellipsoid point with uncertainty ellipse
+    --        (c) Ellipsoid point with altitude and uncertainty ellipsoid
+    --        (d) Ellipsoid Arc
+    --        (e) Ellipsoid Point
+    -- Any other value in octet 1 shall be treated as invalid
+    -- Octets 2 to 8 for case (a) - Ellipsoid point with uncertainty circle
+    --        Degrees of Latitude                3 octets
+    --        Degrees of Longitude               3 octets
+    --        Uncertainty code                   1 octet
+    -- Octets 2 to 11 for case (b) - Ellipsoid point with uncertainty ellipse:
+    --        Degrees of Latitude                3 octets
+    --        Degrees of Longitude               3 octets
+    --        Uncertainty semi-major axis        1 octet
+    --        Uncertainty semi-minor axis        1 octet
+    --        Angle of major axis                1 octet
+    --        Confidence                         1 octet
+    -- Octets 2 to 14 for case (c) - Ellipsoid point with altitude and uncertainty ellipsoid
+    --        Degrees of Latitude                3 octets
+    --        Degrees of Longitude               3 octets
+    --        Altitude                           2 octets
+    --        Uncertainty semi-major axis        1 octet
+    --        Uncertainty semi-minor axis        1 octet
+    --        Angle of major axis                1 octet
+    --        Uncertainty altitude               1 octet
+    --        Confidence                         1 octet
+    -- Octets 2 to 13 for case (d) - Ellipsoid Arc
+    --        Degrees of Latitude                3 octets
+    --        Degrees of Longitude               3 octets
+    --        Inner radius                       2 octets
+    --        Uncertainty radius                 1 octet
+    --        Offset angle                       1 octet
+    --        Included angle                     1 octet
+    --        Confidence                         1 octet
+    -- Octets 2 to 7 for case (e) - Ellipsoid Point
+    --        Degrees of Latitude                3 octets
+    --        Degrees of Longitude               3 octets
+    --
+    -- An Ext-GeographicalInformation parameter comprising more than one octet and
+    -- containing any other shape or an incorrect number of octets or coding according
+    -- to 3G TS 23.032 shall be treated as invalid data by a receiver.
+    --
+    -- An Ext-GeographicalInformation parameter comprising one octet shall be discarded
+    -- by the receiver if an Add-GeographicalInformation parameter is received
+    -- in the same message.
+    --
+    -- An Ext-GeographicalInformation parameter comprising one octet shall be treated as
+    -- invalid data by the receiver if an Add-GeographicalInformation parameter is not
+    -- received in the same message.
+
+-- maxExt-GeographicalInformation  INTEGER ::= 20
+    -- the maximum length allows for further shapes in 3G TS 23.032 to be included in later
+    -- versions of 3G TS 29.002
+
+EquipmentType           ::= ENUMERATED -- INTEGER
+{
+    conferenceBridge    (0)
+}
+
+FileType                ::= ENUMERATED -- INTEGER
+{
+    callRecords         (1),
+    traceRecords        (9),
+    observedIMEITicket  (14)
+}
+
+Fnur                            ::= ENUMERATED
+{
+    --
+    -- See Bearer Capability TS 24.008
+    --
+    fnurNotApplicable                   (0),
+    fnur9600-BitsPerSecond        (1),
+    fnur14400BitsPerSecond        (2),
+    fnur19200BitsPerSecond        (3),
+    fnur28800BitsPerSecond        (4),
+    fnur38400BitsPerSecond        (5),
+    fnur48000BitsPerSecond        (6),
+    fnur56000BitsPerSecond        (7),
+    fnur64000BitsPerSecond        (8),
+    fnur33600BitsPerSecond        (9),
+    fnur32000BitsPerSecond        (10),
+    fnur31200BitsPerSecond        (11)
+}
+
+ForwardToNumber         ::= AddressString
+
+FreeFormatData          ::= OCTET STRING -- (SIZE(1..160))
+    --
+    -- Free formated data as sent in the FCI message
+    -- See TS 29.078
+    --
+
+GenericNumber            ::= BCDDirectoryNumber
+
+GenericNumbers           ::= SET OF GenericNumber
+
+Gsm-SCFAddress           ::= ISDNAddressString
+    --
+    -- See TS 29.002
+    --
+
+HLRIntResult             ::= Diagnostics
+
+Horizontal-Accuracy      ::= OCTET STRING -- (SIZE (1))
+    -- bit 8 = 0
+    -- bits 7-1 = 7 bit Uncertainty Code defined in 3G TS 23.032. The horizontal location
+    -- error should be less than the error indicated by the uncertainty code with 67%
+    -- confidence.
+
+HotBillingTag            ::= ENUMERATED --INTEGER
+{
+    noHotBilling        (0),
+    hotBilling          (1)
+}
+
+HSCSDParmsChange        ::= SEQUENCE
+{
+    changeTime              [0] TimeStamp,
+    hSCSDChanAllocated      [1] NumOfHSCSDChanAllocated,
+    initiatingParty         [2] InitiatingParty                 OPTIONAL,
+    aiurRequested           [3] AiurRequested                   OPTIONAL,
+    chanCodingUsed          [4] ChannelCoding,
+    hSCSDChanRequested      [5] NumOfHSCSDChanRequested         OPTIONAL
+}
+
+
+IMEI ::= TBCD-STRING -- (SIZE (8))
+    --    Refers to International Mobile Station Equipment Identity
+    --    and Software Version Number (SVN) defined in TS GSM 03.03.
+    --    If the SVN is not present the last octet shall contain the
+    --    digit 0 and a filler.
+    --    If present the SVN shall be included in the last octet.
+
+IMSI ::= TBCD-STRING -- (SIZE (3..8))
+    -- digits of MCC, MNC, MSIN are concatenated in this order.
+
+IMEICheckEvent            ::= ENUMERATED -- INTEGER
+{
+    mobileOriginatedCall    (0),
+    mobileTerminatedCall    (1),
+    smsMobileOriginating    (2),
+    smsMobileTerminating    (3),
+    ssAction                (4),
+    locationUpdate          (5)
+}
+
+IMEIStatus                ::= ENUMERATED
+{
+    greyListedMobileEquipment      (0),
+    blackListedMobileEquipment     (1),
+    nonWhiteListedMobileEquipment  (2)
+}
+
+IMSIorIMEI               ::= CHOICE
+{
+    imsi                [0] IMSI,
+    imei                [1] IMEI
+}
+
+InitiatingParty           ::= ENUMERATED
+{
+    network               (0),
+    subscriber            (1)
+}
+
+ISDN-AddressString ::=     AddressString -- (SIZE (1..maxISDN-AddressLength))
+    -- This type is used to represent ISDN numbers.
+
+-- maxISDN-AddressLength  INTEGER ::= 9
+
+LCSCause    ::= OCTET STRING -- (SIZE(1))
+    --
+    -- See LCS Cause Value, 3GPP TS 49.031
+    --
+
+LCS-Priority ::= OCTET STRING -- (SIZE (1))
+    -- 0 = highest priority
+    -- 1 = normal priority
+    -- all other values treated as 1
+
+LCSClientIdentity         ::= SEQUENCE
+{
+    lcsClientExternalID    [0] LCSClientExternalID        OPTIONAL,
+    lcsClientDialedByMS    [1] AddressString              OPTIONAL,
+    lcsClientInternalID    [2] LCSClientInternalID        OPTIONAL
+}
+
+LCSClientExternalID ::= SEQUENCE
+{
+    externalAddress        [0] AddressString          OPTIONAL
+--  extensionContainer     [1] ExtensionContainer         OPTIONAL
+}
+
+LCSClientInternalID ::= ENUMERATED
+{
+    broadcastService          (0),
+    o-andM-HPLMN              (1),
+    o-andM-VPLMN              (2),
+    anonymousLocation         (3),
+    targetMSsubscribedService (4)
+}
+    -- for a CAMEL phase 3 PLMN operator client, the value targetMSsubscribedService shall be used
+
+LCSClientType ::= ENUMERATED
+{
+    emergencyServices         (0),
+    valueAddedServices        (1),
+    plmnOperatorServices      (2),
+    lawfulInterceptServices   (3)
+}
+    --    exception handling:
+    --    unrecognized values may be ignored if the LCS client uses the privacy override
+    --    otherwise, an unrecognized value shall be treated as unexpected data by a receiver
+    --    a return error shall then be returned if received in a MAP invoke
+
+LCSQoSInfo ::= SEQUENCE
+{
+    horizontal-accuracy             [0] Horizontal-Accuracy      OPTIONAL,
+    verticalCoordinateRequest       [1] NULL                     OPTIONAL,
+    vertical-accuracy               [2] Vertical-Accuracy        OPTIONAL,
+    responseTime                    [3] ResponseTime             OPTIONAL
+}
+
+LevelOfCAMELService        ::= BIT STRING
+--     {
+--         basic                         (0),
+--         callDurationSupervision       (1),
+--         onlineCharging                (2)
+--     }
+
+LocationAreaAndCell        ::= SEQUENCE
+{
+    locationAreaCode      [0] LocationAreaCode,
+    cellIdentifier        [1] CellId
+--
+-- For 2G the content of the Cell Identifier is defined by the Cell Id
+-- refer TS 24.008 and for 3G by the Service Area Code refer TS 25.413.
+--
+
+}
+
+LocationAreaCode        ::= OCTET STRING -- (SIZE(2))
+    --
+    -- See TS 24.008
+    --
+
+LocationChange            ::= SEQUENCE
+{
+    location              [0] LocationAreaAndCell,
+    changeTime            [1] TimeStamp
+}
+
+Location-info            ::= SEQUENCE
+{
+    mscNumber             [1] MscNo                    OPTIONAL,
+        location-area             [2] LocationAreaCode,
+    cell-identification   [3] CellId                   OPTIONAL
+}
+
+LocationType ::= SEQUENCE
+{
+locationEstimateType             [0] LocationEstimateType,
+    deferredLocationEventType    [1] DeferredLocationEventType      OPTIONAL
+}
+
+LocationEstimateType ::= ENUMERATED
+{
+    currentLocation                 (0),
+    currentOrLastKnownLocation      (1),
+    initialLocation                 (2),
+    activateDeferredLocation        (3),
+    cancelDeferredLocation          (4)
+}
+    --    exception handling:
+    --    a ProvideSubscriberLocation-Arg containing an unrecognized LocationEstimateType
+    --    shall be rejected by the receiver with a return error cause of unexpected data value
+
+LocUpdResult            ::= Diagnostics
+
+ManagementExtensions    ::= SET OF ManagementExtension
+
+ManagementExtension ::= SEQUENCE
+{
+        identifier    OBJECT IDENTIFIER,
+        significance       [1] BOOLEAN , -- DEFAULT FALSE,
+        information        [2] OCTET STRING
+}
+
+
+MCCMNC    ::= OCTET STRING -- (SIZE(3))
+    --
+    -- This type contains the mobile country code (MCC) and the mobile
+    -- network code (MNC) of a PLMN.
+    --
+
+RateIndication             ::= OCTET STRING -- (SIZE(1))
+
+--0     no rate adaption
+--1     V.110, I.460/X.30
+--2     ITU-T X.31 flag stuffing
+--3     V.120
+--7     H.223 & H.245
+--11    PIAFS
+
+
+MessageReference         ::= OCTET STRING
+
+Month                    ::= INTEGER -- (1..12)
+
+MOLR-Type                ::= INTEGER
+--0            locationEstimate
+--1            assistanceData
+--2            deCipheringKeys
+
+MSCAddress               ::= AddressString
+
+MscNo                    ::= ISDN-AddressString
+    --
+    -- See TS 23.003
+    --
+
+MSISDN                   ::= ISDN-AddressString
+    --
+    -- See TS 23.003
+    --
+
+MSPowerClasses           ::= SET OF RFPowerCapability
+
+NetworkCallReference     ::= CallReferenceNumber
+    -- See TS 29.002
+    --
+
+NetworkSpecificCode      ::= INTEGER
+    --
+    -- To be defined by network operator
+    --
+
+NetworkSpecificServices    ::= SET OF NetworkSpecificCode
+
+NotificationToMSUser ::= ENUMERATED
+{
+    notifyLocationAllowed                          (0),
+    notifyAndVerify-LocationAllowedIfNoResponse    (1),
+    notifyAndVerify-LocationNotAllowedIfNoResponse (2),
+    locationNotAllowed                             (3)
+}
+    -- exception handling:
+    -- At reception of any other value than the ones listed the receiver shall ignore
+    -- NotificationToMSUser.
+
+NumberOfForwarding ::= INTEGER -- (1..5)
+
+NumOfHSCSDChanRequested     ::= INTEGER
+
+NumOfHSCSDChanAllocated     ::= INTEGER
+
+ObservedIMEITicketEnable    ::= BOOLEAN
+
+OriginalCalledNumber        ::= BCDDirectoryNumber
+
+OriginDestCombinations      ::= SET OF OriginDestCombination
+
+OriginDestCombination       ::= SEQUENCE
+{
+    origin                   [0] INTEGER   OPTIONAL,
+    destination              [1] INTEGER   OPTIONAL
+    --
+    -- Note that these values correspond to the contents
+    -- of the attributes originId and destinationId
+    -- respectively. At least one of the two must be present.
+    --
+}
+
+PartialRecordTimer       ::= INTEGER
+
+PartialRecordType        ::= ENUMERATED
+{
+    timeLimit                       (0),
+    serviceChange                   (1),
+    locationChange                  (2),
+    classmarkChange                 (3),
+    aocParmChange                   (4),
+    radioChannelChange              (5),
+    hSCSDParmChange                 (6),
+    changeOfCAMELDestination        (7),
+    firstHotBill                    (20),
+    severalSSOperationBill          (21)
+}
+
+PartialRecordTypes        ::= SET OF PartialRecordType
+
+PositioningData           ::= OCTET STRING -- (SIZE(1..33))
+    --
+    -- See Positioning Data IE (octet 3..n), 3GPP TS 49.031
+    --
+
+RadioChannelsRequested    ::= SET OF RadioChanRequested
+
+RadioChanRequested        ::= ENUMERATED
+{
+    --
+    -- See Bearer Capability TS 24.008
+    --
+    halfRateChannel            (0),
+    fullRateChannel            (1),
+    dualHalfRatePreferred      (2),
+    dualFullRatePreferred      (3)
+}
+
+--RecordClassDestination    ::= CHOICE
+--{
+--    osApplication            [0] AE-title,
+--    fileType                 [1] FileType
+--}
+
+--RecordClassDestinations   ::= SET OF RecordClassDestination
+
+RecordingEntity         ::= AddressString
+
+RecordingMethod         ::= ENUMERATED
+{
+    inCallRecord        (0),
+    inSSRecord          (1)
+}
+
+RedirectingNumber         ::= BCDDirectoryNumber
+
+RedirectingCounter        ::= INTEGER
+
+ResponseTime ::= SEQUENCE
+{
+    responseTimeCategory    ResponseTimeCategory
+}
+    --    note: an expandable SEQUENCE simplifies later addition of a numeric response time.
+
+ResponseTimeCategory ::= ENUMERATED
+{
+    lowdelay          (0),
+    delaytolerant     (1)
+}
+    --    exception handling:
+    --    an unrecognized value shall be treated the same as value 1 (delaytolerant)
+
+RFPowerCapability        ::= INTEGER
+    --
+    -- This field contains the RF power capability of the Mobile station
+    -- classmark 1 and 2 of TS 24.008 expressed as an integer.
+    --
+
+RoamingNumber            ::= ISDN-AddressString
+    --
+    -- See TS 23.003
+    --
+
+RoutingNumber            ::= CHOICE
+{
+    roaming              [1] RoamingNumber,
+    forwarded            [2] ForwardToNumber
+}
+
+Service                  ::= CHOICE
+{
+    teleservice               [1] TeleserviceCode,
+    bearerService             [2] BearerServiceCode,
+    supplementaryService      [3] SS-Code,
+    networkSpecificService    [4] NetworkSpecificCode
+}
+
+ServiceDistanceDependencies    ::= SET OF ServiceDistanceDependency
+
+ServiceDistanceDependency    ::= SEQUENCE
+{
+        aocService                              [0] INTEGER,
+    chargingZone            [1] INTEGER        OPTIONAL
+    --
+    -- Note that these values correspond to the contents
+    -- of the attributes aocServiceId and zoneId
+    -- respectively.
+    --
+}
+
+ServiceKey ::= INTEGER -- (0..2147483647)
+
+SimpleIntegerName            ::= INTEGER
+
+SimpleStringName            ::= GraphicString
+
+SMSResult                    ::= Diagnostics
+
+SmsTpDestinationNumber ::= OCTET STRING
+    --
+    -- This type contains the binary coded decimal representation of
+    -- the SMS address field the encoding of the octet string is in
+    -- accordance with the definition of address fields in TS 23.040.
+    -- This encoding includes type of number and numbering plan indication
+    -- together with the address value range.
+    --
+
+SpeechVersionIdentifier    ::= OCTET STRING -- (SIZE(1))
+--    see GSM 08.08
+
+--    000 0001    GSM speech full rate version 1
+--    001 0001    GSM speech full rate version 2      used for enhanced full rate
+--    010 0001    GSM speech full rate version 3     for future use
+--    000 0101    GSM speech half rate version 1
+--    001 0101    GSM speech half rate version 2     for future use
+--    010 0101    GSM speech half rate version 3    for future use
+
+SSActionResult              ::= Diagnostics
+
+SSActionType                ::= ENUMERATED
+{
+    registration              (0),
+    erasure                   (1),
+    activation                (2),
+    deactivation              (3),
+    interrogation             (4),
+    invocation                (5),
+    passwordRegistration      (6),
+    ussdInvocation            (7)
+}
+
+-- ussdInvocation          (7) include ussd phase 1,phase 2
+
+--SS Request = SSActionType
+
+SS-Code ::= OCTET STRING -- (SIZE (1))
+    -- This type is used to represent the code identifying a single
+    -- supplementary service, a group of supplementary services, or
+    -- all supplementary services. The services and abbreviations
+    -- used are defined in TS 3GPP TS 22.004 [5]. The internal structure is
+    -- defined as follows:
+    --
+    -- bits 87654321: group (bits 8765), and specific service
+    -- (bits 4321)  ussd = ff
+
+--    allSS                   (0x00),
+--        reserved for possible future use
+--        all SS
+--
+--    allLineIdentificationSS (0x10),
+--         reserved for possible future use
+--         all line identification SS
+--
+--    calling-line-identification-presentation                    (0x11),
+--         calling line identification presentation
+--    calling-line-identification-restriction                     (0x12),
+--         calling line identification restriction
+--    connected-line-identification-presentation                  (0x13),
+--         connected line identification presentation
+--    connected-line-identification-restriction                   (0x14),
+--        connected line identification restriction
+--    malicious-call-identification                               (0x15),
+--         reserved for possible future use
+--         malicious call identification
+--
+--    allNameIdentificationSS (0x18),
+--        all name identification SS
+--    calling-name-presentation                    (0x19),
+--         calling name presentation
+--
+--         SS-Codes '00011010'B, to '00011111'B, are reserved for future
+--        NameIdentification Supplementary Service use.
+--
+--    allForwardingSS       (0x20),
+--         all forwarding SS
+--    call-forwarding-unconditional                   (0x21),
+--        call forwarding unconditional
+--    call-deflection                                 (0x24),
+--         call deflection
+--    allCondForwardingSS                             (0x28),
+--        all conditional forwarding SS
+--    call-forwarding-on-mobile-subscriber-busy       (0x29),
+--        call forwarding on mobile subscriber busy
+--    call-forwarding-on-no-reply                     (0x2a),
+--        call forwarding on no reply
+--    call-forwarding-on-mobile-subscriber-not-reachable                 (0x2b),
+--       call forwarding on mobile subscriber not reachable
+--
+--    allCallOfferingSS     (0x30),
+--        reserved for possible future use
+--         all call offering SS includes also all forwarding SS
+--
+--    explicit-call-transfer                   (0x31),
+--            explicit call transfer
+--    mobile-access-hunting                    (0x32),
+--        reserved for possible future use
+--         mobile access hunting
+--
+--    allCallCompletionSS   (0x40),
+--        reserved for possible future use
+--        all Call completion SS
+--
+--    call-waiting                    (0x41),
+--         call waiting
+--    call-hold                       (0x42),
+--        call hold
+--    completion-of-call-to-busy-subscribers-originating-side                (0x43),
+--       completion of call to busy subscribers, originating side
+--    completion-of-call-to-busy-subscribers-destination-side                (0x44),
+--        completion of call to busy subscribers, destination side
+--         this SS-Code is used only in InsertSubscriberData and DeleteSubscriberData
+--
+--    multicall                    (0x45),
+--         multicall
+--
+--    allMultiPartySS              (0x50),
+--         reserved for possible future use
+--        all multiparty SS
+--
+--    multiPTY                     (0x51),
+--        multiparty
+--
+--    allCommunityOfInterest-SS           (0x60),
+--        reserved for possible future use
+--         all community of interest SS
+--    closed-user-group                   (0x61),
+--        closed user group
+--
+--    allChargingSS                               (0x70),
+--         reserved for possible future use
+--         all charging SS
+--    advice-of-charge-information                (0x71),
+--        advice of charge information
+--    advice-of-charge-charging                   (0x72),
+--         advice of charge charging
+--
+--    allAdditionalInfoTransferSS    (0x80),
+--         reserved for possible future use
+--         all additional information transfer SS
+--    uUS1-user-to-user-signalling                           (0x81),
+--       UUS1 user-to-user signalling
+--    uUS2-user-to-user-signalling                           (0x82),
+--        UUS2 user-to-user signalling
+--    uUS3-user-to-user-signalling                           (0x83),
+--        UUS3 user-to-user signalling
+--
+--    allBarringSS           (0x90),
+--        all barring SS
+--    barringOfOutgoingCalls (0x91),
+--         barring of outgoing calls
+--    barring-of-all-outgoing-calls                          (0x92),
+--         barring of all outgoing calls
+--    barring-of-outgoing-international-calls                (0x93),
+--         barring of outgoing international calls
+--    boicExHC               (0x94),
+--         barring of outgoing international calls except those directed
+--         to the home PLMN
+--    barringOfIncomingCalls (0x99),
+--         barring of incoming calls
+--    barring-of-all-incoming-calls                          (0x9a),
+--         barring of all incoming calls
+--    barring-of-incoming-calls-when-roaming-outside-home-PLMN-Country       (0x9b),
+--         barring of incoming calls when roaming outside home PLMN
+--         Country
+--
+--    allCallPrioritySS       (0xa0),
+--         reserved for possible future use
+--         all call priority SS
+--    enhanced-Multilevel-Precedence-Pre-emption-EMLPP-service                (0xa1),
+--         enhanced Multilevel Precedence Pre-emption 'EMLPP) service
+--
+--    allLCSPrivacyException (0xb0),
+--         all LCS Privacy Exception Classes
+--    universal              (0xb1),
+--         allow location by any LCS client
+--    callrelated            (0xb2),
+--         allow location by any value added LCS client to which a call
+--         is established from the target MS
+--    callunrelated          (0xb3),
+--         allow location by designated external value added LCS clients
+--    plmnoperator           (0xb4),
+--         allow location by designated PLMN operator LCS clients
+--
+--    allMOLR-SS                  (0xc0),
+--         all Mobile Originating Location Request Classes
+--    basicSelfLocation           (0xc1),
+--         allow an MS to request its own location
+--    autonomousSelfLocation      (0xc2),
+--         allow an MS to perform self location without interaction
+--         with the PLMN for a predetermined period of time
+--    transferToThirdParty        (0xc3),
+--         allow an MS to request transfer of its location to another LCS client
+--
+--    allPLMN-specificSS      (0xf0),
+--    plmn-specificSS-1       (0xf1),
+--    plmn-specificSS-2       (0xf2),
+--    plmn-specificSS-3       (0xf3),
+--    plmn-specificSS-4       (0xf4),
+--    plmn-specificSS-5       (0xf5),
+--    plmn-specificSS-6       (0xf6),
+--    plmn-specificSS-7       (0xf7),
+--    plmn-specificSS-8       (0xf8),
+--    plmn-specificSS-9       (0xf9),
+--    plmn-specificSS-A       (0xfa),
+--    plmn-specificSS-B       (0xfb),
+--    plmn-specificSS-C       (0xfc),
+--    plmn-specificSS-D       (0xfd),
+--    plmn-specificSS-E       (0xfe),
+--    ussd                    (0xff)
+
+
+SSParameters                ::= CHOICE
+{
+    forwardedToNumber       [0] ForwardToNumber,
+    unstructuredData        [1] OCTET STRING
+}
+
+SupplServices               ::= SET OF SS-Code
+
+SuppServiceUsed             ::= SEQUENCE
+{
+    ssCode                  [0] SS-Code         OPTIONAL,
+    ssTime                  [1] TimeStamp       OPTIONAL
+}
+
+SwitchoverTime              ::= SEQUENCE
+{
+    hour                    INTEGER , -- (0..23),
+    minute                  INTEGER , -- (0..59),
+    second                  INTEGER -- (0..59)
+}
+
+SystemType  ::= ENUMERATED
+    --  "unknown" is not to be used in PS domain.
+{
+    unknown                (0),
+    iuUTRAN                (1),
+    gERAN                  (2)
+}
+
+TBCD-STRING ::= OCTET STRING
+    -- This type (Telephony Binary Coded Decimal String) is used to
+    -- represent several digits from 0 through 9, *, #, a, b, c, two
+    -- digits per octet, each digit encoded 0000 to 1001 (0 to 9),
+    -- 1010 (*), 1011 (#), 1100 (a), 1101 (b) or 1110 (c); 1111 used
+    -- as filler when there is an odd number of digits.
+
+    -- bits 8765 of octet n encoding digit 2n
+    -- bits 4321 of octet n encoding digit 2(n-1) +1
+
+TariffId                    ::= INTEGER
+
+TariffPeriod                ::= SEQUENCE
+{
+    switchoverTime            [0] SwitchoverTime,
+    tariffId                  [1] INTEGER
+    -- Note that the value of tariffId corresponds
+    -- to the attribute tariffId.
+}
+
+TariffPeriods                 ::= SET OF TariffPeriod
+
+TariffSystemStatus            ::= ENUMERATED
+{
+    available           (0),    -- available for modification
+    checked             (1),    -- "frozen" and checked
+    standby             (2),    -- "frozen" awaiting activation
+    active              (3)     -- "frozen" and active
+}
+
+
+TimeStamp                    ::= OCTET STRING -- (SIZE(9))
+    --
+    -- The contents of this field are a compact form of the UTCTime format
+    -- containing local time plus an offset to universal time. Binary coded
+    -- decimal encoding is employed for the digits to reduce the storage and
+    -- transmission overhead
+    -- e.g. YYMMDDhhmmssShhmm
+    -- where
+    -- YY    =    Year 00 to 99        BCD encoded
+    -- MM    =    Month 01 to 12       BCD encoded
+    -- DD    =    Day 01 to 31         BCD encoded
+    -- hh    =    hour 00 to 23        BCD encoded
+    -- mm    =    minute 00 to 59      BCD encoded
+    -- ss    =    second 00 to 59      BCD encoded
+    -- S     =    Sign 0 = "+", "-"    ASCII encoded
+    -- hh    =    hour 00 to 23        BCD encoded
+    -- mm    =    minute 00 to 59      BCD encoded
+    --
+
+TrafficChannel          ::=    ENUMERATED
+{
+    fullRate            (0),
+    halfRate            (1)
+}
+
+TranslatedNumber        ::=     BCDDirectoryNumber
+
+TransparencyInd         ::=    ENUMERATED
+{
+    transparent         (0),
+    nonTransparent      (1)
+}
+
+ROUTE                   ::=     CHOICE
+{
+    rOUTENumber         [0] INTEGER,
+    rOUTEName           [1] GraphicString
+}
+
+--rOUTEName  1  10 octet
+
+TSChangeover            ::=    SEQUENCE
+{
+    newActiveTS            [0] INTEGER,
+    newStandbyTS           [1] INTEGER,
+--    changeoverTime       [2] GeneralizedTime   OPTIONAL,
+    authkey                [3] OCTET STRING      OPTIONAL,
+    checksum               [4] OCTET STRING      OPTIONAL,
+    versionNumber          [5] OCTET STRING      OPTIONAL
+    -- Note that if the changeover time is not
+    -- specified then the change is immediate.
+}
+
+TSCheckError            ::=    SEQUENCE
+{
+    errorId               [0] TSCheckErrorId
+    --fail                [1] ANY DEFINED BY errorId      OPTIONAL
+}
+
+TSCheckErrorId          ::=    CHOICE
+{
+    globalForm            [0] OBJECT IDENTIFIER,
+    localForm             [1] INTEGER
+}
+
+TSCheckResult           ::=    CHOICE
+{
+    success             [0] NULL,
+    fail                [1] SET OF TSCheckError
+}
+
+TSCopyTariffSystem       ::=    SEQUENCE
+{
+    oldTS                [0] INTEGER,
+    newTS                [1] INTEGER
+}
+
+TSNextChange            ::=    CHOICE
+{
+    noChangeover        [0] NULL,
+    tsChangeover        [1] TSChangeover
+}
+
+TypeOfSubscribers       ::= ENUMERATED
+{
+    home                (0),    -- HPLMN subscribers
+    visiting            (1),    -- roaming subscribers
+    all                 (2)
+}
+
+TypeOfTransaction       ::=    ENUMERATED
+{
+    successful          (0),
+    unsuccessful        (1),
+    all                 (2)
+}
+
+Vertical-Accuracy ::= OCTET STRING -- (SIZE (1))
+    -- bit 8 = 0
+    -- bits 7-1 = 7 bit Vertical Uncertainty Code defined in 3G TS 23.032.
+    -- The vertical location error should be less than the error indicated
+    -- by the uncertainty code with 67% confidence.
+
+ISDNAddressString ::= AddressString
+
+EmlppPriority ::= OCTET STRING -- (SIZE (1))
+
+--priorityLevelA    EMLPP-Priority ::= 6
+--priorityLevelB    EMLPP-Priority ::= 5
+--priorityLevel0    EMLPP-Priority ::= 0
+--priorityLevel1    EMLPP-Priority ::= 1
+--priorityLevel2    EMLPP-Priority ::= 2
+--priorityLevel3    EMLPP-Priority ::= 3
+--priorityLevel4    EMLPP-Priority ::= 4
+--See 29.002
+
+
+EASubscriberInfo ::= OCTET STRING -- (SIZE (3))
+        -- The internal structure is defined by the Carrier Identification
+    -- parameter in ANSI T1.113.3. Carrier codes between "000" and "999" may
+    -- be encoded as 3 digits using "000" to "999" or as 4 digits using
+    -- "0000" to "0999". Carrier codes between "1000" and "9999" are encoded
+    -- using 4 digits.
+
+SelectedCIC ::= OCTET STRING -- (SIZE (3))
+
+PortedFlag       ::=    ENUMERATED
+{
+    numberNotPorted        (0),
+    numberPorted           (1)
+}
+
+SubscriberCategory   ::= OCTET STRING -- (SIZE (1))
+-- unknownuser   = 0x00,
+-- frenchuser    = 0x01,
+-- englishuser   = 0x02,
+-- germanuser    = 0x03,
+-- russianuser   = 0x04,
+-- spanishuser   = 0x05,
+-- specialuser   = 0x06,
+-- reserveuser   = 0x09,
+-- commonuser    = 0x0a,
+-- superioruser  = 0x0b,
+-- datacalluser  = 0x0c,
+-- testcalluser  = 0x0d,
+-- spareuser     = 0x0e,
+-- payphoneuser  = 0x0f,
+-- coinuser      = 0x20,
+-- isup224       = 0xe0
+
+
+CUGOutgoingAccessIndicator ::=    ENUMERATED
+{
+    notCUGCall  (0),
+    cUGCall     (1)
+}
+
+CUGInterlockCode ::= OCTET STRING -- (SIZE (4))
+
+--
+
+CUGOutgoingAccessUsed ::= ENUMERATED
+{
+    callInTheSameCUGGroup      (0),
+    callNotInTheSameCUGGroup   (1)
+}
+
+SMSTEXT        ::= OCTET STRING
+
+MSCCIC         ::= INTEGER -- (0..65535)
+
+RNCorBSCId     ::= OCTET STRING -- (SIZE (3))
+--octet order is the same as RANAP/BSSAP signaling
+--if spc is coded as 14bit, then OCTET STRING1 will filled with 00 ,for example rnc id = 123 will be coded as 00 01 23
+--OCTET STRING1
+--OCTET STRING2
+--OCTET STRING3
+
+MSCId          ::= OCTET STRING -- (SIZE (3))
+--National network format , octet order is the same as ISUP signaling
+--if spc is coded as 14bit, then OCTET STRING1 will filled with 00,,for example rnc id = 123 will be coded as 00 01 23
+--OCTET STRING1
+--OCTET STRING2
+--OCTET STRING3
+
+EmergencyCallFlag ::= ENUMERATED
+{
+    notEmergencyCall  (0),
+    emergencyCall     (1)
+}
+
+CUGIncomingAccessUsed ::= ENUMERATED
+{
+    callInTheSameCUGGroup      (0),
+    callNotInTheSameCUGGroup   (1)
+}
+
+SmsUserDataType               ::= OCTET STRING -- (SIZE (1))
+--
+--00  concatenated-short-messages-8-bit-reference-number
+--01  special-sms-message-indication
+--02  reserved
+--03  Value not used to avoid misinterpretation as <LF>
+--04  characterapplication-port-addressing-scheme-8-bit-address
+--05  application-port-addressing-scheme-16-bit-address
+--06  smsc-control-parameters
+--07  udh-source-indicator
+--08  concatenated-short-message-16-bit-reference-number
+--09  wireless-control-message-protocol
+--0A  text-formatting
+--0B  predefined-sound
+--0C  user-defined-sound-imelody-max-128-bytes
+--0D  predefined-animation
+--0E  large-animation-16-16-times-4-32-4-128-bytes
+--0F  small-animation-8-8-times-4-8-4-32-bytes
+--10  large-picture-32-32-128-bytes
+--11  small-picture-16-16-32-bytes
+--12  variable-picture
+--13  User prompt indicator
+--14  Extended Object
+--15  Reused Extended Object
+--16  Compression Control
+--17  Object Distribution Indicator
+--18  Standard WVG object
+--19  Character Size WVG object
+--1A  Extended Object Data Request Command
+--1B-1F    Reserved for future EMS features (see subclause 3.10)
+--20    RFC 822 E-Mail Header
+--21    Hyperlink format element
+--22    Reply Address Element
+--23 - 6F    Reserved for future use
+--70 - 7F    (U)SIM Toolkit Security Headers
+--80 - 9F    SME to SME specific use
+--A0 - BF    Reserved for future use
+--C0 - DF    SC specific use
+--E0 - FE    Reserved for future use
+--FF          normal SMS
+
+ConcatenatedSMSReferenceNumber              ::=  INTEGER -- (0..65535)
+
+MaximumNumberOfSMSInTheConcatenatedSMS      ::=  INTEGER -- (0..255)
+
+SequenceNumberOfTheCurrentSMS               ::=  INTEGER -- (0..255)
+
+SequenceNumber       ::=  INTEGER
+
+--(1...   )
+--
+
+DisconnectParty             ::= ENUMERATED
+{
+      callingPartyRelease           (0),
+      calledPartyRelease            (1),
+      networkRelease                (2)
+}
+
+ChargedParty     ::= ENUMERATED
+{
+      callingParty           (0),
+      calledParty            (1)
+}
+
+ChargeAreaCode                      ::=  OCTET STRING -- (SIZE (1..3))
+
+CUGIndex                            ::=  OCTET STRING -- (SIZE (2))
+
+GuaranteedBitRate                   ::= ENUMERATED
+{
+     gBR14400BitsPerSecond (1),        -- BS20 non-transparent
+     gBR28800BitsPerSecond (2),        -- BS20 non-transparent and transparent,
+                                      -- BS30 transparent and multimedia
+     gBR32000BitsPerSecond (3),        -- BS30 multimedia
+     gBR33600BitsPerSecond (4),        -- BS30 multimedia
+     gBR56000BitsPerSecond (5),        -- BS30 transparent and multimedia
+     gBR57600BitsPerSecond (6),        -- BS20 non-transparent
+     gBR64000BitsPerSecond (7),        -- BS30 transparent and multimedia
+
+     gBR12200BitsPerSecond (106),      -- AMR speech
+     gBR10200BitsPerSecond (107),      -- AMR speech
+     gBR7950BitsPerSecond (108),        -- AMR speech
+     gBR7400BitsPerSecond (109),        -- AMR speech
+     gBR6700BitsPerSecond (110),        -- AMR speech
+     gBR5900BitsPerSecond (111),        -- AMR speech
+     gBR5150BitsPerSecond (112),        -- AMR speech
+     gBR4750BitsPerSecond (113)         -- AMR speech
+}
+
+MaximumBitRate                  ::= ENUMERATED
+{
+     mBR14400BitsPerSecond (1),         -- BS20 non-transparent
+     mBR28800BitsPerSecond (2),         -- BS20 non-transparent and transparent,
+                                 -- BS30 transparent and multimedia
+     mBR32000BitsPerSecond (3),         -- BS30 multimedia
+     mBR33600BitsPerSecond (4),         -- BS30 multimedia
+     mBR56000BitsPerSecond (5),         -- BS30 transparent and multimedia
+     mBR57600BitsPerSecond (6),         -- BS20 non-transparent
+     mBR64000BitsPerSecond (7),         -- BS30 transparent and multimedia
+
+     mBR12200BitsPerSecond (106),      -- AMR speech
+     mBR10200BitsPerSecond (107),      -- AMR speech
+     mBR7950BitsPerSecond (108),        -- AMR speech
+     mBR7400BitsPerSecond (109),        -- AMR speech
+     mBR6700BitsPerSecond (110),        -- AMR speech
+     mBR5900BitsPerSecond (111),        -- AMR speech
+     mBR5150BitsPerSecond (112),        -- AMR speech
+     mBR4750BitsPerSecond (113)         -- AMR speech
+}
+
+
+HLC          ::= OCTET STRING
+
+-- this parameter is a 1:1 copy of the contents (i.e. starting with octet 3) of the "high layer compatibility" parameter of ITU-T Q.931 [35].
+
+LLC          ::= OCTET STRING
+
+-- this parameter is a 1:1 copy of the contents (i.e. starting with octet 3) of the "low layer compatibility" parameter of ITU-T Q.931 [35].
+
+
+ISDN-BC      ::= OCTET STRING
+
+-- this parameter is a 1:1 copy of the contents (i.e. starting with octet 3) of the "bearer capability" parameter of ITU-T Q.931 [35].
+
+ModemType           ::= ENUMERATED
+{
+    none-modem                  (0),
+    modem-v21                   (1),
+    modem-v22                   (2),
+    modem-v22-bis               (3),
+    modem-v23                   (4),
+    modem-v26-ter               (5),
+    modem-v32                   (6),
+    modem-undef-interface       (7),
+    modem-autobauding1          (8),
+    no-other-modem-type        (31),
+    modem-v34                  (33)
+}
+
+UssdCodingScheme            ::= OCTET STRING
+
+UssdString                  ::= OCTET STRING
+
+UssdNotifyCounter           ::=  INTEGER -- (0..255)
+
+UssdRequestCounter          ::=  INTEGER -- (0..255)
+
+Classmark3                  ::= OCTET STRING -- (SIZE(2))
+
+OptimalRoutingDestAddress   ::= BCDDirectoryNumber
+
+GAI                         ::= OCTET STRING -- (SIZE(7))
+--such as 64 F0 00 00 ABCD 1234
+
+ChangeOfglobalAreaID        ::= SEQUENCE
+{
+    location                [0] GAI,
+    changeTime              [1] TimeStamp
+}
+
+InteractionWithIP  ::=  NULL
+
+RouteAttribute     ::=  ENUMERATED
+{
+    cas    (0),
+    tup    (1),
+    isup   (2),
+    pra    (3),
+    bicc   (4),
+    sip    (5),
+    others (255)
+}
+
+VoiceIndicator  ::=    ENUMERATED
+{
+    sendToneByLocalMsc (0) ,
+    sendToneByOtherMsc (1),
+    voiceNoIndication  (3)
+}
+
+BCategory  ::=    ENUMERATED
+{
+    subscriberFree         (0),
+    subscriberBusy         (1),
+    subscriberNoIndication (3)
+}
+
+CallType   ::=    ENUMERATED
+{
+     unknown     (0),
+     internal    (1),
+     incoming    (2),
+     outgoing    (3),
+     tandem      (4)
+}
+
+-- END
+END
+}
+
+1;
+
index 862018e..aa94630 100644 (file)
@@ -20,7 +20,9 @@ use FS::cdr qw(_cdr_date_parser_maker);
       my($cdr, $field, $conf, $hashref) = @_;
       $hashref->{skiprow} = 1
         unless ($field == 0 && $cdr->disposition == 100       )  #regular CDR
-            || ($field == 1 && $cdr->lastapp     eq 'acctcode'); #accountcode
+            || ($field == 1 && $cdr->lastapp     eq 'acctcode')  #accountcode
+            || ($field == 1 && $cdr->lastapp     eq 'CallerId')  #CID blocking
+            ;
       $cdr->cdrtypenum($field);
     },
 
index 9e644db..603d5c4 100644 (file)
@@ -19,7 +19,7 @@ my %cdr_type_of = (
 
 %info = (
   'name'          => 'Telstra LinxOnline',
-  'weight'        => 20,
+  'weight'        => 215,
   'header'        => 1,
   'type'          => 'fixedlength',
   # Wholesale Usage Information Record format
diff --git a/FS/FS/cdr/u4.pm b/FS/FS/cdr/u4.pm
new file mode 100644 (file)
index 0000000..1b7a660
--- /dev/null
@@ -0,0 +1,104 @@
+package FS::cdr::u4;
+
+use strict;
+use vars qw(@ISA %info);
+use FS::cdr qw(_cdr_date_parser_maker);
+
+@ISA = qw(FS::cdr);
+
+%info = (
+  'name'          => 'U4',
+  'weight'        => 490,
+  'type'          => 'fixedlength',
+  'fixedlength_format' => [qw(
+    CDRType:3:1:3
+    MasterAccountID:12:4:15
+    SubAccountID:12:16:27
+    BillToNumber:18:28:45
+    AccountCode:12:46:57
+    CallDateStartTime:14:58:71
+    TimeOfDay:1:72:72
+    CalculatedSeconds:12:73:84
+    City:30:85:114
+    State:2:115:116
+    Country:40:117:156
+    Charges:21:157:177
+    CallDirection:1:178:178
+    CallIndicator:1:179:179
+    ReportIndicator:1:180:180
+    ANI:10:181:190
+    DNIS:10:191:200
+    PIN:16:201:216
+    OrigNumber:10:217:226
+    TermNumber:10:227:236
+    DialedNumber:18:237:254
+    DisplayNumber:18:255:272
+    RecordSource:1:273:273
+    LECInfoDigits:2:274:275
+    OrigNPA:4:276:279
+    OrigNXX:5:280:284
+    OrigLATA:3:285:287
+    OrigZone:1:288:288
+    OrigCircuit:12:289:300
+    OrigTrunkGroupCLLI:12:301:312
+    TermNPA:4:313:316
+    TermNXX:5:317:321
+    TermLATA:3:322:324
+    TermZone:1:325:325
+    TermCircuit:12:326:337
+    TermTrunkGroupCLLI:12:338:349
+    TermOCN:5:350:354
+  )],
+  # at least that's how they're defined in the spec we have.
+  # the real CDRs have several differences.
+  'import_fields' => [
+    '',               #CDRType (for now always 'V')
+    '',               #MasterAccountID
+    '',               #SubAccountID
+    'charged_party',  #BillToNumber
+    'accountcode',    #AccountCode
+    _cdr_date_parser_maker('startdate'),
+                      #CallDateTime
+    '',               #TimeOfDay (always 'S')
+    sub {             #CalculatedSeconds
+      my($cdr, $sec) = @_;
+      $cdr->duration($sec);
+      $cdr->billsec($sec);
+    },
+    '',               #City
+    '',               #State
+    '',               #Country
+    'upstream_price', #Charges
+    sub {             #CallDirection
+      my ($cdr, $dir) = @_;
+      $cdr->set('direction', $dir);
+      if ( $dir eq 'O' ) {
+        $cdr->set('src', $cdr->charged_party);
+      } elsif ( $dir eq 'I' ) {
+        $cdr->set('dst', $cdr->charged_party);
+      }
+    },
+    '',               #CallIndicator  #calltype?
+    '',               #ReportIndicator
+    sub {             #ANI
+      # it appears that it's the "other" number, not necessarily ANI.
+      my ($cdr, $number) = @_;
+      if ( $cdr->direction eq 'O' ) {
+        $cdr->set('dst', $number);
+      } elsif ( $cdr->direction eq 'I' ) {
+        $cdr->set('src', $number);
+      }
+    },
+    '',               #DNIS
+    '',               #PIN
+    '',               #OrigNumber
+    '',               #TermNumber
+    '',               #DialedNumber
+    '',               #DisplayNumber
+    '',               #RecordSource
+    '',               #LECInfoDigits
+    ('') x 13,
+  ],
+);
+
+1;
diff --git a/FS/FS/cdr_cust_pkg_usage.pm b/FS/FS/cdr_cust_pkg_usage.pm
new file mode 100644 (file)
index 0000000..6ef7f2d
--- /dev/null
@@ -0,0 +1,124 @@
+package FS::cdr_cust_pkg_usage;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::cdr_cust_pkg_usage - Object methods for cdr_cust_pkg_usage records
+
+=head1 SYNOPSIS
+
+  use FS::cdr_cust_pkg_usage;
+
+  $record = new FS::cdr_cust_pkg_usage \%hash;
+  $record = new FS::cdr_cust_pkg_usage { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cdr_cust_pkg_usage object represents an allocation of included 
+usage minutes to a call.  FS::cdr_cust_pkg_usage inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item cdrusagenum - primary key
+
+=item acctid - foreign key to cdr.acctid
+
+=item pkgusagenum - foreign key to cust_pkg_usage.pkgusagenum
+
+=item minutes - the number of minutes allocated
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example.  To add the example to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'cdr_cust_pkg_usage'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('cdrusagenum')
+    || $self->ut_foreign_key('acctid', 'cdr', 'acctid')
+    || $self->ut_foreign_key('pkgusagenum', 'cust_pkg_usage', 'pkgusagenum')
+    || $self->ut_number('minutes')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=item cust_pkg_usage
+
+Returns the L<FS::cust_pkg_usage> object that this usage allocation came from.
+
+=item cdr
+
+Returns the L<FS::cdr> object that the usage was applied to.
+
+=cut
+
+sub cust_pkg_usage {
+  FS::cust_pkg_usage->by_key($_[0]->pkgusagenum);
+}
+
+sub cdr {
+  FS::cdr->by_key($_[0]->acctid);
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index f84af42..8fcd724 100644 (file)
@@ -326,8 +326,8 @@ sub check {
     || $self->ut_foreign_keyn('custnum',     'cust_main',     'custnum')
     || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
     || $self->ut_foreign_keyn('classnum',    'contact_class', 'classnum')
-    || $self->ut_textn('last')
-    || $self->ut_textn('first')
+    || $self->ut_namen('last')
+    || $self->ut_namen('first')
     || $self->ut_textn('title')
     || $self->ut_textn('comment')
     || $self->ut_enum('disabled', [ '', 'Y' ])
diff --git a/FS/FS/contact_Mixin.pm b/FS/FS/contact_Mixin.pm
new file mode 100644 (file)
index 0000000..6e8f315
--- /dev/null
@@ -0,0 +1,19 @@
+package FS::contact_Mixin;
+
+use strict;
+use FS::Record qw( qsearchs );
+use FS::contact;
+
+=item contact_obj
+
+Returns the contact object, if any (see L<FS::contact>).
+
+=cut
+
+sub contact_obj {
+  my $self = shift;
+  return '' unless $self->contactnum;
+  qsearchs( 'contact', { 'contactnum' => $self->contactnum } );
+}
+
+1;
index e7622d7..8b156c6 100644 (file)
@@ -1330,6 +1330,8 @@ invoice and all older invoices is greater than the specified amount.
 
 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
 
+I<lpr>, if specified, is passed to 
+
 =cut
 
 sub queueable_send {
@@ -1354,6 +1356,7 @@ sub send {
   my( $template, $invoice_from, $notice_name );
   my $agentnums = '';
   my $balance_over = 0;
+  my $lpr = '';
 
   if ( ref($_[0]) ) {
     my $opt = shift;
@@ -1364,6 +1367,7 @@ sub send {
     $invoice_from = $opt->{'invoice_from'};
     $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
     $notice_name = $opt->{'notice_name'};
+    $lpr = $opt->{'lpr'}
   } else {
     $template = scalar(@_) ? shift : '';
     if ( scalar(@_) && $_[0]  ) {
@@ -1397,10 +1401,12 @@ sub send {
     if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
     && ! $self->invoice_noemail;
 
+  $opt{'lpr'} = $lpr;
   #$self->print_invoice(\%opt)
   $self->print(\%opt)
     if grep { $_ eq 'POST' } @invoicing_list; #postal
 
+  #this has never been used post-$ORIGINAL_ISP afaik
   $self->fax_invoice(\%opt)
     if grep { $_ eq 'FAX' } @invoicing_list; #fax
 
@@ -1564,14 +1570,16 @@ sub print {
   return if $self->hide;
   my $conf = $self->conf;
 
-  my( $template, $notice_name );
+  my( $template, $notice_name, $lpr );
   if ( ref($_[0]) ) {
     my $opt = shift;
     $template = $opt->{'template'} || '';
     $notice_name = $opt->{'notice_name'} || 'Invoice';
+    $lpr = $opt->{'lpr'}
   } else {
     $template = scalar(@_) ? shift : '';
     $notice_name = 'Invoice';
+    $lpr = '';
   }
 
   my %opt = (
@@ -1584,7 +1592,11 @@ sub print {
     $self->batch_invoice(\%opt);
   }
   else {
-    do_print $self->lpr_data(\%opt);
+    do_print(
+      $self->lpr_data(\%opt),
+      'agentnum' => $self->cust_main->agentnum,
+      'lpr'      => $lpr,
+    );
   }
 }
 
@@ -2118,10 +2130,13 @@ sub print_csv {
     $previous_balance = sprintf('%.2f', $previous_balance);
     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
     my @items = map {
-      ($_->{pkgnum} || ''),
-      $_->{description},
-      $_->{amount}
-    } $self->_items_pkg;
+                      $_->{pkgnum},
+                      $_->{description},
+                      $_->{amount}
+                    }
+                  $self->_items_pkg, #_items_nontax?  no sections or anything
+                                     # with this format
+                  $self->_items_tax;
 
     $csv->combine(
       $cust_main->agentnum,
@@ -3122,11 +3137,16 @@ sub _items_payments {
 
     #something more elaborate if $_->amount ne ->cust_pay->paid ?
 
+    my $desc = $self->mt('Payment received').' '.
+               time2str($date_format,$_->cust_pay->_date );
+    $desc   .= $self->mt(' via ' . $_->cust_pay->payby_payinfo_pretty)
+      if ( $self->conf->exists('invoice_payment_details') );
     push @b, {
-      'description' => $self->mt('Payment received').' '.
-                       time2str($date_format,$_->cust_pay->_date ),
+      'description' => $desc,
       'amount'      => sprintf("%.2f", $_->amount )
     };
+
   }
 
   @b;
index 716c098..d8cbf59 100644 (file)
@@ -1104,8 +1104,7 @@ sub upgrade_tax_location {
     delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
 
     $hash{custnum} = $h_cust_main->custnum;
-    my $tax_loc = qsearchs('cust_location', \%hash) # unlikely
-                  || FS::cust_location->new({ %hash });
+    my $tax_loc = FS::cust_location->new_or_existing(\%hash);
     if ( !$tax_loc->locationnum ) {
       $tax_loc->disabled('Y');
       my $error = $tax_loc->insert;
index 723d6e0..140982e 100644 (file)
@@ -215,10 +215,8 @@ sub cust_credit_bill_pkg {
 
 sub cust_main_county {
   my $self = shift;
-  my $result;
-  if ( $self->taxtype eq 'FS::cust_main_county' ) {
-    $result = qsearchs( 'cust_main_county', { 'taxnum' => $self->taxnum } );
-  }
+  return '' unless $self->taxtype eq 'FS::cust_main_county';
+  qsearchs( 'cust_main_county', { 'taxnum' => $self->taxnum } );
 }
 
 sub _upgrade_data {
index 05d961c..ba279a2 100644 (file)
@@ -717,7 +717,7 @@ sub credit_lineitems {
   my %cust_bill_pkg = ();
   my %cust_credit_bill_pkg = ();
   my %taxlisthash = ();
-  my %unapplied_payments; #invoice numbers, and then billpaynums
+  my %unapplied_payments = (); #invoice numbers, and then billpaynums
   foreach my $billpkgnum ( @{$arg{billpkgnums}} ) {
     my $setuprecur = shift @{$arg{setuprecurs}};
     my $amount = shift @{$arg{amounts}};
index 7427d09..3cb44a0 100644 (file)
@@ -348,13 +348,13 @@ sub cust_bill_pkg {
 
 sub cust_bill_pkg_tax_Xlocation {
   my $self = shift;
-  if ($self->billpkg_tax_locationnum) {
+  if ($self->billpkgtaxlocationnum) {
     return qsearchs(
       'cust_bill_pkg_tax_location',
       { 'billpkgtaxlocationnum' => $self->billpkgtaxlocationnum },
     );
  
-  } elsif ($self->billpkg_tax_rate_locationnum) {
+  } elsif ($self->billpkgtaxratelocationnum) {
     return qsearchs(
       'cust_bill_pkg_tax_rate_location',
       { 'billpkgtaxratelocationnum' => $self->billpkgtaxratelocationnum },
index b86529b..b12a161 100644 (file)
@@ -5,7 +5,7 @@ use strict;
 use vars qw( $import );
 use Locale::Country;
 use FS::UID qw( dbh driver_name );
-use FS::Record qw( qsearch ); #qsearchs );
+use FS::Record qw( qsearch qsearchs );
 use FS::Conf;
 use FS::prospect_main;
 use FS::cust_main;
@@ -104,6 +104,35 @@ points to.  You can ask the object for a copy with the I<hash> method.
 
 sub table { 'cust_location'; }
 
+=item new_or_existing HASHREF
+
+Returns an existing location matching the customer and address fields in 
+HASHREF, if one exists; otherwise returns a new location containing those 
+fields.  The following fields must match: address1, address2, city, county,
+state, zip, country, geocode, disabled.  Other fields are only required
+to match if they're specified in HASHREF.
+
+The new location will not be inserted; the calling code must call C<insert>
+(or a method such as C<move_to>) to insert it, and check for errors at that
+point.
+
+=cut
+
+sub new_or_existing {
+  my $class = shift;
+  my %hash = ref($_[0]) ? %{$_[0]} : @_;
+  # if coords are empty, then it doesn't matter if they're auto or not
+  if ( !$hash{'latitude'} and !$hash{'longitude'} ) {
+    delete $hash{'coord_auto'};
+  }
+  foreach ( qw(address1 address2 city county state zip country geocode
+              disabled ) ) {
+    # empty fields match only empty fields
+    $hash{$_} = '' if !defined($hash{$_});
+  }
+  return qsearchs('cust_location', \%hash) || $class->new(\%hash);
+}
+
 =item insert
 
 Adds this record to the database.  If there is an error, returns the error,
@@ -479,6 +508,20 @@ sub location_label {
   $prefix . $self->SUPER::location_label(%opt);
 }
 
+=item county_state_county
+
+Returns a string consisting of just the county, state and country.
+
+=cut
+
+sub county_state_country {
+  my $self = shift;
+  my $label = $self->country;
+  $label = $self->state.", $label" if $self->state;
+  $label = $self->county." County, $label" if $self->county;
+  $label;
+}
+
 =back
 
 =head1 CLASS METHODS
index 45d57cd..2a4602e 100644 (file)
@@ -2,7 +2,6 @@ package FS::cust_main;
 
 require 5.006;
 use strict;
-             #FS::cust_main:_Marketgear when they're ready to move to 2.1
 use base qw( FS::cust_main::Packages FS::cust_main::Status
              FS::cust_main::NationalID
              FS::cust_main::Billing FS::cust_main::Billing_Realtime
@@ -551,14 +550,6 @@ sub insert {
     }
   }
 
-  if ( $self->can('start_copy_skel') ) {
-    my $error = $self->start_copy_skel;
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return $error;
-    }
-  }
-
   warn "  ordering packages\n"
     if $DEBUG > 1;
 
@@ -1787,12 +1778,19 @@ sub check {
     || $self->ut_floatn('credit_limit')
     || $self->ut_numbern('billday')
     || $self->ut_numbern('prorate_day')
-    || $self->ut_enum('edit_subject', [ '', 'Y' ] )
-    || $self->ut_enum('calling_list_exempt', [ '', 'Y' ] )
-    || $self->ut_enum('invoice_noemail', [ '', 'Y' ] )
+    || $self->ut_flag('edit_subject')
+    || $self->ut_flag('calling_list_exempt')
+    || $self->ut_flag('invoice_noemail')
+    || $self->ut_flag('message_noemail')
     || $self->ut_enum('locale', [ '', FS::Locales->locales ])
   ;
 
+  my $company = $self->company;
+  $company =~ s/^\s+//; 
+  $company =~ s/\s+$//; 
+  $company =~ s/\s+/ /g;
+  $self->company($company);
+
   #barf.  need message catalogs.  i18n.  etc.
   $error .= "Please select an advertising source."
     if $error =~ /^Illegal or empty \(numeric\) refnum: /;
@@ -4086,15 +4084,34 @@ sub ship_contact_firstlast {
   $contact->get('first') . ' '. $contact->get('last');
 }
 
-=item country_full
+#XXX this doesn't work in 3.x+
+#=item country_full
+#
+#Returns this customer's full country name
+#
+#=cut
+#
+#sub country_full {
+#  my $self = shift;
+#  code2country($self->country);
+#}
+
+=item county_state_county [ PREFIX ]
 
-Returns this customer's full country name
+Returns a string consisting of just the county, state and country.
 
 =cut
 
-sub country_full {
+sub county_state_country {
   my $self = shift;
-  code2country($self->country);
+  my $locationnum;
+  if ( @_ && $_[0] && $self->has_ship_address ) {
+    $locationnum = $self->ship_locationnum;
+  } else {
+    $locationnum = $self->bill_locationnum;
+  }
+  my $cust_location = qsearchs('cust_location', { locationnum=>$locationnum });
+  $cust_location->county_state_country;
 }
 
 =item geocode DATA_VENDOR
@@ -4917,7 +4934,10 @@ sub queueable_print {
 
 sub print {
   my ($self, $template) = (shift, shift);
-  do_print [ $self->print_ps($template) ];
+  do_print(
+    [ $self->print_ps($template) ],
+    'agentnum' => $self->agentnum,
+  );
 }
 
 #these three subs should just go away once agent stuff is all config overrides
@@ -5059,12 +5079,12 @@ sub process_censustract_update {
 }
 
 #starting to take quite a while for big dbs
+#   (JRNL: journaled so it only happens once per database)
 # - seq scan of h_cust_main (yuck), but not going to index paycvv, so
-# - seq scan of cust_main on signupdate... index signupdate?  will that help?
-# - seq scan of cust_main on paydate... index on substrings?  maybe set an
-#    upgrade journal flag now that we have that, yyyy-m-dd paydates are ancient
-# - seq scan of cust_main on payinfo.. certainly not going toi ndex that...
-#    upgrade journal again?  this is also an ancient problem
+# JRNL seq scan of cust_main on signupdate... index signupdate?  will that help?
+# JRNL seq scan of cust_main on paydate... index on substrings?  maybe set an
+# JRNL seq scan of cust_main on payinfo.. certainly not going toi ndex that...
+# JRNL leading/trailing spaces in first, last, company
 # - otaker upgrade?  journal and call it good?  (double check to make sure
 #    we're not still setting otaker here)
 #
@@ -5119,10 +5139,30 @@ sub _upgrade_data { #class method
   local($ignore_banned_card) = 1;
   local($skip_fuzzyfiles) = 1;
   local($import) = 1; #prevent automatic geocoding (need its own variable?)
-  $class->_upgrade_otaker(%opts);
 
   FS::cust_main::Location->_upgrade_data(%opts);
 
+  unless ( FS::upgrade_journal->is_done('cust_main__trimspaces') ) {
+
+    foreach my $cust_main ( qsearch({
+      'table'     => 'cust_main', 
+      'hashref'   => {},
+      'extra_sql' => 'WHERE '.
+                       join(' OR ',
+                         map "$_ LIKE ' %' OR $_ LIKE '% ' OR $_ LIKE '%  %'",
+                           qw( first last company )
+                       ),
+    }) ) {
+      my $error = $cust_main->replace;
+      die $error if $error;
+    }
+
+    FS::upgrade_journal->set_done('cust_main__trimspaces');
+
+  }
+
+  $class->_upgrade_otaker(%opts);
+
 }
 
 =back
index cd46c73..939a625 100644 (file)
@@ -116,8 +116,13 @@ sub bill_and_collect {
   $options{'actual_time'} ||= time;
   my $job = $options{'job'};
 
+  my $actual_time = ( $conf->exists('next-bill-ignore-time')
+                        ? day_end( $options{actual_time} )
+                        : $options{actual_time}
+                    );
+
   $job->update_statustext('0,cleaning expired packages') if $job;
-  $error = $self->cancel_expired_pkgs( day_end( $options{actual_time} ) );
+  $error = $self->cancel_expired_pkgs( $actual_time );
   if ( $error ) {
     $error = "Error expiring custnum ". $self->custnum. ": $error";
     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
@@ -125,7 +130,7 @@ sub bill_and_collect {
     else                                                     { warn   $error; }
   }
 
-  $error = $self->suspend_adjourned_pkgs( day_end( $options{actual_time} ) );
+  $error = $self->suspend_adjourned_pkgs( $actual_time );
   if ( $error ) {
     $error = "Error adjourning custnum ". $self->custnum. ": $error";
     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
@@ -133,7 +138,7 @@ sub bill_and_collect {
     else                                                     { warn   $error; }
   }
 
-  $error = $self->unsuspend_resumed_pkgs( day_end( $options{actual_time} ) );
+  $error = $self->unsuspend_resumed_pkgs( $actual_time );
   if ( $error ) {
     $error = "Error resuming custnum ".$self->custnum. ": $error";
     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
@@ -410,6 +415,7 @@ sub bill {
   my @precommit_hooks = ();
 
   $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ];  #param checks?
+
   foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
 
     next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
@@ -914,6 +920,11 @@ sub _make_lines {
 
   $cust_pkg->pkgpart($part_pkg->pkgpart);
 
+  my $cmp_time = ( $conf->exists('next-bill-ignore-time')
+                     ? day_end( $time )
+                     : $time
+                 );
+
   ###
   # bill setup
   ###
@@ -927,7 +938,7 @@ sub _make_lines {
        and ( $options{'resetup'}
              || ( ! $cust_pkg->setup
                   && ( ! $cust_pkg->start_date
-                       || $cust_pkg->start_date <= day_end($time)
+                       || $cust_pkg->start_date <= $cmp_time
                      )
                   && ( ! $conf->exists('disable_setup_suspended_pkgs')
                        || ( $conf->exists('disable_setup_suspended_pkgs') &&
@@ -975,7 +986,7 @@ sub _make_lines {
                                      && ! $cust_pkg->option('no_suspend_bill',1)
                                   )
        and
-            ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= day_end($time) )
+            ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $cmp_time )
          || ( $part_pkg->plan eq 'voip_cdr'
                && $part_pkg->option('bill_every_call')
             )
@@ -999,7 +1010,7 @@ sub _make_lines {
 
     #over two params!  lets at least switch to a hashref for the rest...
     my $increment_next_bill = ( $part_pkg->freq ne '0'
-                                && ( $cust_pkg->getfield('bill') || 0 ) <= day_end($time)
+                                && ( $cust_pkg->getfield('bill') || 0 ) <= $cmp_time
                                 && !$options{cancel}
                               );
     my %param = ( %setup_param,
@@ -1027,13 +1038,35 @@ sub _make_lines {
       if ( $@ );
 
     #base_cancel???
-    $unitrecur = $cust_pkg->part_pkg->base_recur || $recur; #XXX uuh
+    $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better
 
     if ( $increment_next_bill ) {
 
-      my $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
+      my $next_bill;
+
+      if ( my $main_pkg = $cust_pkg->main_pkg ) {
+        # supplemental package
+        # to keep in sync with the main package, simulate billing at 
+        # its frequency
+        my $main_pkg_freq = $main_pkg->part_pkg->freq;
+        my $supp_pkg_freq = $part_pkg->freq;
+        my $ratio = $supp_pkg_freq / $main_pkg_freq;
+        if ( $ratio != int($ratio) ) {
+          # the UI should prevent setting up packages like this, but just
+          # in case
+          return "supplemental package period is not an integer multiple of main  package period";
+        }
+        $next_bill = $sdate;
+        for (1..$ratio) {
+          $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq );
+        }
+
+      } else {
+        # the normal case
+      $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
       return "unparsable frequency: ". $part_pkg->freq
         if $next_bill == -1;
+      }  
   
       #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
       # only for figuring next bill date, nothing else, so, reset $sdate again
@@ -1796,8 +1829,9 @@ sub due_cust_event {
 
   #???
   #my $DEBUG = $opt{'debug'}
+  $opt{'debug'} ||= 0; # silence some warnings
   local($DEBUG) = $opt{'debug'}
-    if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG;
+    if $opt{'debug'} > $DEBUG;
   $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
 
   warn "$me due_cust_event called with options ".
index ba3513b..bd0af53 100644 (file)
@@ -167,15 +167,29 @@ sub _upgrade_data {
     $cust_main->set(bill_locationnum => $bill_location->locationnum);
 
     if ( $cust_main->get('ship_address1') ) {
-      my $ship_location = FS::cust_location->new(
-        {
-          custnum => $custnum,
-          map { $_ => $cust_main->get("ship_$_") } location_fields()
+      # detect duplicates
+      my $same = 1;
+      my $ship_location;
+      foreach (location_fields()) {
+        if ( length($cust_main->get("ship_$_")) and
+             $cust_main->get($_) ne $cust_main->get("ship_$_") ) {
+          $same = 0;
         }
-      );
-      $error = $ship_location->insert;
-      die "error migrating service address for customer $custnum: $error"
-        if $error;
+      }
+
+      if ( $same ) {
+        $ship_location = $bill_location;
+      } else {
+        $ship_location = FS::cust_location->new(
+          {
+            custnum => $custnum,
+            map { $_ => $cust_main->get("ship_$_") } location_fields()
+          }
+        );
+        $error = $ship_location->insert;
+        die "error migrating service address for customer $custnum: $error"
+          if $error;
+      }
 
       $cust_main->set(ship_locationnum => $ship_location->locationnum);
 
index 395cce7..f83bce9 100644 (file)
@@ -29,6 +29,9 @@ These methods are available on FS::cust_main objects;
 
 Orders a single package.
 
+Note that if the package definition has supplemental packages, those will
+be ordered as well.
+
 Options may be passed as a list of key/value pairs or as a hash reference.
 Options are:
 
@@ -84,7 +87,7 @@ sub order_pkg {
     if exists($opt->{'depend_jobnum'}) && $opt->{'depend_jobnum'};
 
   my %insert_params = map { $opt->{$_} ? ( $_ => $opt->{$_} ) : () }
-                          qw( ticket_subject ticket_queue );
+                          qw( ticket_subject ticket_queue allow_pkgpart );
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
@@ -97,17 +100,48 @@ sub order_pkg {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  if ( $opt->{'cust_location'} &&
-       ( ! $cust_pkg->locationnum || $cust_pkg->locationnum == -1 ) ) {
-    my $error = $opt->{'cust_location'}->insert;
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return "inserting cust_location (transaction rolled back): $error";
+  if ( $opt->{'contactnum'} and $opt->{'contactnum'} != -1 ) {
+
+    $cust_pkg->contactnum($opt->{'contactnum'});
+
+  } elsif ( $opt->{'contact'} ) {
+
+    if ( ! $opt->{'contact'}->contactnum ) {
+      # not inserted yet
+      my $error = $opt->{'contact'}->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "inserting contact (transaction rolled back): $error";
+      }
     }
-    $cust_pkg->locationnum($opt->{'cust_location'}->locationnum);
+    $cust_pkg->contactnum($opt->{'contact'}->contactnum);
+
+  #} else {
+  #
+  #  $cust_pkg->contactnum();
+
   }
-  else {
+
+  if ( $opt->{'locationnum'} and $opt->{'locationnum'} != -1 ) {
+
+    $cust_pkg->locationnum($opt->{'locationnum'});
+
+  } elsif ( $opt->{'cust_location'} ) {
+
+    if ( ! $opt->{'cust_location'}->locationnum ) {
+      # not inserted yet
+      my $error = $opt->{'cust_location'}->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "inserting cust_location (transaction rolled back): $error";
+      }
+    }
+    $cust_pkg->locationnum($opt->{'cust_location'}->locationnum);
+
+  } else {
+
     $cust_pkg->locationnum($self->ship_locationnum);
+
   }
 
   $cust_pkg->custnum( $self->custnum );
@@ -141,6 +175,35 @@ sub order_pkg {
     }
   }
 
+  # add supplemental packages, if any are needed
+  my $part_pkg = FS::part_pkg->by_key($cust_pkg->pkgpart);
+  foreach my $link ($part_pkg->supp_part_pkg_link) {
+    #warn "inserting supplemental package ".$link->dst_pkgpart;
+    my $pkg = FS::cust_pkg->new({
+        'pkgpart'       => $link->dst_pkgpart,
+        'pkglinknum'    => $link->pkglinknum,
+        'custnum'       => $self->custnum,
+        'main_pkgnum'   => $cust_pkg->pkgnum,
+        'locationnum'   => $cust_pkg->locationnum,
+        # try to prevent as many surprises as possible
+        'pkgbatch'      => $cust_pkg->pkgbatch,
+        'start_date'    => $cust_pkg->start_date,
+        'order_date'    => $cust_pkg->order_date,
+        'expire'        => $cust_pkg->expire,
+        'adjourn'       => $cust_pkg->adjourn,
+        'contract_end'  => $cust_pkg->contract_end,
+        'refnum'        => $cust_pkg->refnum,
+        'discountnum'   => $cust_pkg->discountnum,
+        'waive_setup'   => $cust_pkg->waive_setup,
+        'allow_pkgpart' => $opt->{'allow_pkgpart'},
+    });
+    $error = $self->order_pkg('cust_pkg' => $pkg);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "inserting supplemental package: $error";
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   ''; #no error
 
index 1047890..7dbb7a8 100644 (file)
@@ -18,7 +18,8 @@ use FS::svc_acct;
 $DEBUG = 0;
 $me = '[FS::cust_main::Search]';
 
-@fuzzyfields = ( 'first', 'last', 'company', 'address1' );
+@fuzzyfields = ( 'cust_main.first', 'cust_main.last', 'cust_main.company', 
+  'cust_location.address1' );
 
 install_callback FS::UID sub { 
   $conf = new FS::Conf;
@@ -339,7 +340,7 @@ sub smart_search {
       my %fuzopts = (
         'hashref'   => \%options,
         'select'    => '',
-        'extra_sql' => " AND $agentnums_sql",    #agent virtualization
+        'extra_sql' => "WHERE $agentnums_sql",    #agent virtualization
       );
 
       if ( $first && $last ) {
@@ -355,7 +356,8 @@ sub smart_search {
       }
       if ( $conf->exists('address1-search') ) {
         push @cust_main,
-          FS::cust_main::Search->fuzzy_search( { 'address1' => $value }, %fuzopts );
+          FS::cust_main::Search->fuzzy_search(
+            { 'cust_location.address1' => $value }, %fuzopts );
       }
 
     }
@@ -644,6 +646,16 @@ sub search {
     if $params->{'with_email'};
 
   ##
+  # "with postal mail invoices" checkbox
+  ##
+
+  push @where,
+    "EXISTS ( SELECT 1 FROM cust_main_invoice
+                WHERE cust_main_invoice.custnum = cust_main.custnum
+                  AND dest = 'POST' )"
+    if $params->{'POST'};
+
+  ##
   # "without postal mail invoices" checkbox
   ##
 
@@ -792,11 +804,19 @@ sub search {
     @tagnums = grep /^(\d+)$/, @tagnums;
 
     if ( @tagnums ) {
+      if ( $params->{'all_tags'} ) {
+        foreach ( @tagnums ) {
+          push @where, 'exists(select 1 from cust_tag where '.
+                       'cust_tag.custnum = cust_main.custnum and tagnum = '.
+                       $_ . ')';
+        }
+      } else { # matching any tag, not all
        my $tags_where = "0 < (select count(1) from cust_tag where " 
                . " cust_tag.custnum = cust_main.custnum and tagnum in ("
                . join(',', @tagnums) . "))";
 
        push @where, $tags_where;
+      }
     }
   }
 
@@ -814,6 +834,12 @@ sub search {
   my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
 
   my $addl_from = '';
+  # always make address fields available in results
+  for my $pre ('bill_', 'ship_') {
+    $addl_from .= 
+      'LEFT JOIN cust_location AS '.$pre.'location '.
+      'ON (cust_main.'.$pre.'locationnum = '.$pre.'location.locationnum) ';
+  }
 
   my $count_query = "SELECT COUNT(*) FROM cust_main $extra_sql";
 
@@ -831,7 +857,8 @@ sub search {
   if ($params->{'flattened_pkgs'}) {
 
     #my $pkg_join = '';
-    $addl_from .= ' LEFT JOIN cust_pkg USING ( custnum ) ';
+    $addl_from .=
+      ' LEFT JOIN cust_pkg ON ( cust_main.custnum = cust_pkg.custnum ) ';
 
     if ($dbh->{Driver}->{Name} eq 'Pg') {
 
@@ -914,7 +941,8 @@ Additional options are the same as FS::Record::qsearch
 =cut
 
 sub fuzzy_search {
-  my( $self, $fuzzy ) = @_;
+  my $self = shift;
+  my $fuzzy = shift;
   # sensible defaults, then merge in any passed options
   my %fuzopts = (
     'table'     => 'cust_main',
@@ -926,6 +954,11 @@ sub fuzzy_search {
 
   my @cust_main = ();
 
+  my @fuzzy_mod = 'i';
+  my $conf = new FS::Conf;
+  my $fuzziness = $conf->config('fuzzy-fuzziness');
+  push @fuzzy_mod, $fuzziness if $fuzziness;
+
   check_and_rebuild_fuzzyfiles();
   foreach my $field ( keys %$fuzzy ) {
 
@@ -933,32 +966,31 @@ sub fuzzy_search {
     next unless scalar(@$all);
 
     my %match = ();
-    $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) );
-
-    my @fcust = ();
-    foreach ( keys %match ) {
-      if ( $field eq 'address1' ) {
-        #because it lives outside the table
-        my $addl_from = $fuzopts{addl_from} .
-                        'JOIN cust_location USING (custnum)';
-        my $extra_sql = $fuzopts{extra_sql} .
-                        " AND cust_location.address1 = ".dbh->quote($_);
-        push @fcust, qsearch({
-            %fuzopts,
-            'addl_from' => $addl_from,
-            'extra_sql' => $extra_sql,
-        });
-      } else {
-        my $hash = $fuzopts{hashref};
-        $hash->{$field} = $_;
-        push @fcust, qsearch({
-            %fuzopts,
-            'hashref' => $hash
-        });
-      }
+    $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, \@fuzzy_mod, @$all ) );
+    next if !keys(%match);
+
+    my $in_matches = 'IN (' .
+                     join(',', map { dbh->quote($_) } keys %match) .
+                     ')';
+
+    my $extra_sql = $fuzopts{extra_sql};
+    if ($extra_sql =~ /^\s*where /i or keys %{ $fuzopts{hashref} }) {
+      $extra_sql .= ' AND ';
+    } else {
+      $extra_sql .= 'WHERE ';
+    }
+    $extra_sql .= "$field $in_matches";
+
+    my $addl_from = $fuzopts{addl_from};
+    if ( $field =~ /^cust_location/ ) {
+      $addl_from .= ' JOIN cust_location USING (custnum)';
     }
-    my %fsaw = ();
-    push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust;
+
+    push @cust_main, qsearch({
+      %fuzopts,
+      'addl_from' => $addl_from,
+      'extra_sql' => $extra_sql,
+    });
   }
 
   # we want the components of $fuzzy ANDed, not ORed, but still don't want dupes
@@ -997,28 +1029,29 @@ sub rebuild_fuzzyfiles {
 
   foreach my $fuzzy ( @fuzzyfields ) {
 
-    open(LOCK,">>$dir/cust_main.$fuzzy")
-      or die "can't open $dir/cust_main.$fuzzy: $!";
-    flock(LOCK,LOCK_EX)
-      or die "can't lock $dir/cust_main.$fuzzy: $!";
+    my ($field, $table) = reverse split('\.', $fuzzy);
+    $table ||= 'cust_main';
 
-    open (CACHE, '>:encoding(UTF-8)', "$dir/cust_main.$fuzzy.tmp")
-      or die "can't open $dir/cust_main.$fuzzy.tmp: $!";
+    open(LOCK,">>$dir/$table.$field")
+      or die "can't open $dir/$table.$field: $!";
+    flock(LOCK,LOCK_EX)
+      or die "can't lock $dir/$table.$field: $!";
 
-    foreach my $field ( $fuzzy, "ship_$fuzzy" ) {
-      my $sth = dbh->prepare("SELECT $field FROM cust_main".
-                             " WHERE $field != '' AND $field IS NOT NULL");
-      $sth->execute or die $sth->errstr;
+    open (CACHE, '>:encoding(UTF-8)', "$dir/$table.$field.tmp")
+      or die "can't open $dir/$table.$field.tmp: $!";
 
-      while ( my $row = $sth->fetchrow_arrayref ) {
-        print CACHE $row->[0]. "\n";
-      }
+    my $sth = dbh->prepare(
+      "SELECT $field FROM $table WHERE $field IS NOT NULL AND $field != ''"
+    );
+    $sth->execute or die $sth->errstr;
 
-    } 
+    while ( my $row = $sth->fetchrow_arrayref ) {
+      print CACHE $row->[0]. "\n";
+    }
 
-    close CACHE or die "can't close $dir/cust_main.$fuzzy.tmp: $!";
+    close CACHE or die "can't close $dir/$table.$field.tmp: $!";
   
-    rename "$dir/cust_main.$fuzzy.tmp", "$dir/cust_main.$fuzzy";
+    rename "$dir/$table.$field.tmp", "$dir/$table.$field";
     close LOCK;
   }
 
@@ -1037,20 +1070,24 @@ sub append_fuzzyfiles {
 
   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
 
-  foreach my $field (@fuzzyfields) {
+  foreach my $fuzzy (@fuzzyfields) {
+
+    my ($field, $table) = reverse split('\.', $fuzzy);
+    $table ||= 'cust_main';
+
     my $value = shift;
 
     if ( $value ) {
 
-      open(CACHE, '>>:encoding(UTF-8)', "$dir/cust_main.$field" )
-        or die "can't open $dir/cust_main.$field: $!";
+      open(CACHE, '>>:encoding(UTF-8)', "$dir/$table.$field" )
+        or die "can't open $dir/$table.$field: $!";
       flock(CACHE,LOCK_EX)
-        or die "can't lock $dir/cust_main.$field: $!";
+        or die "can't lock $dir/$table.$field: $!";
 
       print CACHE "$value\n";
 
       flock(CACHE,LOCK_UN)
-        or die "can't unlock $dir/cust_main.$field: $!";
+        or die "can't unlock $dir/$table.$field: $!";
       close CACHE;
     }
 
@@ -1064,10 +1101,13 @@ sub append_fuzzyfiles {
 =cut
 
 sub all_X {
-  my( $self, $field ) = @_;
+  my( $self, $fuzzy ) = @_;
+  my ($field, $table) = reverse split('\.', $fuzzy);
+  $table ||= 'cust_main';
+
   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
-  open(CACHE, '<:encoding(UTF-8)', "$dir/cust_main.$field")
-    or die "can't open $dir/cust_main.$field: $!";
+  open(CACHE, '<:encoding(UTF-8)', "$dir/$table.$field")
+    or die "can't open $dir/$table.$field: $!";
   my @array = map { chomp; $_; } <CACHE>;
   close CACHE;
   \@array;
diff --git a/FS/FS/cust_main/_Marketgear.pm b/FS/FS/cust_main/_Marketgear.pm
deleted file mode 100644 (file)
index 2d3c927..0000000
+++ /dev/null
@@ -1,146 +0,0 @@
-package FS::cust_main::_Marketgear;
-
-use strict;
-use vars qw( $DEBUG $me $conf );
-
-$DEBUG = 0;
-$me = '[FS::cust_main::_Marketgear]';
-
-install_callback FS::UID sub { 
-  $conf = new FS::Conf;
-};
-
-sub start_copy_skel {
-  my $self = shift;
-
-  return '' unless $conf->config('cust_main-skeleton_tables')
-                && $conf->config('cust_main-skeleton_custnum');
-
-  warn "  inserting skeleton records\n"
-    if $DEBUG > 1 || $cust_main::DEBUG > 1;
-
-  #'mg_user_preference' => {},
-  #'mg_user_indicator_profile.user_indicator_profile_id' => { 'mg_profile_indicator.profile_indicator_id' => { 'mg_profile_details.profile_detail_id' }, },
-  #'mg_watchlist_header.watchlist_header_id' => { 'mg_watchlist_details.watchlist_details_id' },
-  #'mg_user_grid_header.grid_header_id' => { 'mg_user_grid_details.user_grid_details_id' },
-  #'mg_portfolio_header.portfolio_header_id' => { 'mg_portfolio_trades.portfolio_trades_id' => { 'mg_portfolio_trades_positions.portfolio_trades_positions_id' } },
-  my @tables = eval(join('\n',$conf->config('cust_main-skeleton_tables')));
-  die $@ if $@;
-
-  _copy_skel( 'cust_main',                                 #tablename
-              $conf->config('cust_main-skeleton_custnum'), #sourceid
-              $self->custnum,                              #destid
-              @tables,                                     #child tables
-            );
-}
-
-#recursive subroutine, not a method
-sub _copy_skel {
-  my( $table, $sourceid, $destid, %child_tables ) = @_;
-
-  my $primary_key;
-  if ( $table =~ /^(\w+)\.(\w+)$/ ) {
-    ( $table, $primary_key ) = ( $1, $2 );
-  } else {
-    my $dbdef_table = dbdef->table($table);
-    $primary_key = $dbdef_table->primary_key
-      or return "$table has no primary key".
-                " (or do you need to run dbdef-create?)";
-  }
-
-  warn "  _copy_skel: $table.$primary_key $sourceid to $destid for ".
-       join (', ', keys %child_tables). "\n"
-    if $DEBUG > 2;
-
-  foreach my $child_table_def ( keys %child_tables ) {
-
-    my $child_table;
-    my $child_pkey = '';
-    if ( $child_table_def =~ /^(\w+)\.(\w+)$/ ) {
-      ( $child_table, $child_pkey ) = ( $1, $2 );
-    } else {
-      $child_table = $child_table_def;
-
-      $child_pkey = dbdef->table($child_table)->primary_key;
-      #  or return "$table has no primary key".
-      #            " (or do you need to run dbdef-create?)\n";
-    }
-
-    my $sequence = '';
-    if ( keys %{ $child_tables{$child_table_def} } ) {
-
-      return "$child_table has no primary key".
-             " (run dbdef-create or try specifying it?)\n"
-        unless $child_pkey;
-
-      #false laziness w/Record::insert and only works on Pg
-      #refactor the proper last-inserted-id stuff out of Record::insert if this
-      # ever gets use for anything besides a quick kludge for one customer
-      my $default = dbdef->table($child_table)->column($child_pkey)->default;
-      $default =~ /^nextval\(\(?'"?([\w\.]+)"?'/i
-        or return "can't parse $child_table.$child_pkey default value ".
-                  " for sequence name: $default";
-      $sequence = $1;
-
-    }
-  
-    my @sel_columns = grep { $_ ne $primary_key }
-                           dbdef->table($child_table)->columns;
-    my $sel_columns = join(', ', @sel_columns );
-
-    my @ins_columns = grep { $_ ne $child_pkey } @sel_columns;
-    my $ins_columns = ' ( '. join(', ', $primary_key, @ins_columns ). ' ) ';
-    my $placeholders = ' ( ?, '. join(', ', map '?', @ins_columns ). ' ) ';
-
-    my $sel_st = "SELECT $sel_columns FROM $child_table".
-                 " WHERE $primary_key = $sourceid";
-    warn "    $sel_st\n"
-      if $DEBUG > 2;
-    my $sel_sth = dbh->prepare( $sel_st )
-      or return dbh->errstr;
-  
-    $sel_sth->execute or return $sel_sth->errstr;
-
-    while ( my $row = $sel_sth->fetchrow_hashref ) {
-
-      warn "    selected row: ".
-           join(', ', map { "$_=".$row->{$_} } keys %$row ). "\n"
-        if $DEBUG > 2;
-
-      my $statement =
-        "INSERT INTO $child_table $ins_columns VALUES $placeholders";
-      my $ins_sth =dbh->prepare($statement)
-          or return dbh->errstr;
-      my @param = ( $destid, map $row->{$_}, @ins_columns );
-      warn "    $statement: [ ". join(', ', @param). " ]\n"
-        if $DEBUG > 2;
-      $ins_sth->execute( @param )
-        or return $ins_sth->errstr;
-
-      #next unless keys %{ $child_tables{$child_table} };
-      next unless $sequence;
-      
-      #another section of that laziness
-      my $seq_sql = "SELECT currval('$sequence')";
-      my $seq_sth = dbh->prepare($seq_sql) or return dbh->errstr;
-      $seq_sth->execute or return $seq_sth->errstr;
-      my $insertid = $seq_sth->fetchrow_arrayref->[0];
-  
-      # don't drink soap!  recurse!  recurse!  okay!
-      my $error =
-        _copy_skel( $child_table_def,
-                    $row->{$child_pkey}, #sourceid
-                    $insertid, #destid
-                    %{ $child_tables{$child_table_def} },
-                  );
-      return $error if $error;
-
-    }
-
-  }
-
-  return '';
-
-}
-
-1;
index 5733595..a61d67e 100644 (file)
@@ -137,33 +137,6 @@ sub check {
 
 }
 
-sub taxname {
-  my $self = shift;
-  if ( $self->dbdef_table->column('taxname') ) {
-    return $self->setfield('taxname', $_[0]) if @_;
-    return $self->getfield('taxname');
-  }  
-  return '';
-}
-
-sub setuptax {
-  my $self = shift;
-  if ( $self->dbdef_table->column('setuptax') ) {
-    return $self->setfield('setuptax', $_[0]) if @_;
-    return $self->getfield('setuptax');
-  }  
-  return '';
-}
-
-sub recurtax {
-  my $self = shift;
-  if ( $self->dbdef_table->column('recurtax') ) {
-    return $self->setfield('recurtax', $_[0]) if @_;
-    return $self->getfield('recurtax');
-  }  
-  return '';
-}
-
 =item label OPTIONS
 
 Returns a label looking like "Anytown, Alameda County, CA, US".
@@ -174,13 +147,10 @@ If the taxname field is set, it will look like
 If the taxclass is set, then it will be
 "Anytown, Alameda County, CA, US (International)".
 
-Currently it will not contain the district, even if the city+county+state
-is not unique.
-
-OPTIONS may contain "no_taxclass" (hides taxclass) and/or "no_city"
-(hides city).  It may also contain "out", in which case, if this 
-region (district+city+county+state+country) contains no non-zero 
-taxes, the label will read "Out of taxable region(s)".
+OPTIONS may contain "with_taxclass", "with_city", and "with_district" to show
+those fields.  It may also contain "out", in which case, if this region 
+(district+city+county+state+country) contains no non-zero taxes, the label 
+will read "Out of taxable region(s)".
 
 =cut
 
@@ -202,12 +172,15 @@ sub label {
   my $label = $self->country;
   $label = $self->state.", $label" if $self->state;
   $label = $self->county." County, $label" if $self->county;
-  if (!$opt{no_city}) {
+  if ($opt{with_city}) {
     $label = $self->city.", $label" if $self->city;
+    if ($opt{with_district} and $self->district) {
+      $label = $self->district . ", $label";
+    }
   }
   # ugly labels when taxclass and taxname are both non-null...
   # but this is how the tax report does it
-  if (!$opt{no_taxclass}) {
+  if ($opt{with_taxclass}) {
     $label = "$label (".$self->taxclass.')' if $self->taxclass;
   }
   $label = $self->taxname." ($label)" if $self->taxname;
@@ -512,8 +485,10 @@ sub taxline {
   # now round and distribute
   my $extra_cents = sprintf('%.2f', $taxable_cents * $self->tax / 100) * 100
                     - $tax_cents;
+  # make sure we have an integer
+  $extra_cents = sprintf('%.0f', $extra_cents);
   if ( $extra_cents < 0 ) {
-    die "nonsense extra_cents value $extra_cents"; # because seriously, wtf
+    die "nonsense extra_cents value $extra_cents";
   }
   $tax_cents += $extra_cents;
   my $i = 0;
index 4535aad..0e9e8a7 100644 (file)
@@ -1032,18 +1032,48 @@ sub _upgrade_data {  #class method
   ###
 
   # not only cust_pay, but also voided and refunded payments
-  if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch')) {
+  if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch_1')) {
+    local $FS::Record::nowarn_classload=1;
     # really inefficient, but again, only has to run once
     foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
+      my $and_batchnum_is_null =
+        ( $table =~ /^cust_pay/ ? ' AND batchnum IS NULL' : '' );
       foreach my $object ( qsearch({
             table     => $table,
             extra_sql => "WHERE payby IN('CARD','CHEK') ".
-                         "AND paybatch IS NOT NULL",
+                         "AND (paybatch IS NOT NULL ".
+                         "OR (paybatch IS NULL AND auth IS NULL
+                         $and_batchnum_is_null ) )",
           }) )
       {
+        if ( $object->paybatch eq '' ) {
+          # repair for a previous upgrade that didn't save 'auth'
+          my $pkey = $object->primary_key;
+          # find the last history record that had a paybatch value
+          my $h = qsearchs({
+              table   => "h_$table",
+              hashref => {
+                $pkey     => $object->$pkey,
+                paybatch  => { op=>'!=', value=>''},
+                history_action => 'replace_old',
+              },
+              order_by => 'ORDER BY history_date DESC LIMIT 1',
+          });
+          if (!$h) {
+            warn "couldn't find paybatch history record for $table ".$object->$pkey."\n";
+            next;
+          }
+          # if the paybatch didn't have an auth string, then it's fine
+          $h->paybatch =~ /:(\w+):/ or next;
+          # set paybatch to what it was in that record
+          $object->set('paybatch', $h->paybatch)
+          # and then upgrade it like the old records
+        }
+
         my $parsed = $object->_parse_paybatch;
         if (keys %$parsed) {
           $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
+          $object->set('auth' => $parsed->{authorization});
           $object->set('paybatch', '');
           my $error = $object->replace;
           warn "error parsing CARD/CHEK paybatch fields on $object #".
@@ -1052,7 +1082,7 @@ sub _upgrade_data {  #class method
         }
       } #$object
     } #$table
-    FS::upgrade_journal->set_done('cust_pay__parse_paybatch');
+    FS::upgrade_journal->set_done('cust_pay__parse_paybatch_1');
   }
 }
 
index 9f2e9dd..e1e32d3 100644 (file)
@@ -9,7 +9,7 @@ use FS::payinfo_Mixin;
 use FS::cust_main;
 use FS::cust_bill;
 
-@ISA = qw( FS::payinfo_Mixin FS::Record );
+@ISA = qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record );
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -80,7 +80,9 @@ following fields are currently supported:
 
 =item country 
 
-=item status
+=item status - 'Approved' or 'Declined'
+
+=item error_message - the error returned by the gateway if any
 
 =back
 
@@ -289,19 +291,21 @@ sub retriable {
   '';
 }
 
-=item approve PAYBATCH
+=item approve OPTIONS
 
 Approve this payment.  This will replace the existing record with the 
 same paybatchnum, set its status to 'Approved', and generate a payment 
 record (L<FS::cust_pay>).  This should only be called from the batch 
 import process.
 
+OPTIONS may contain "gatewaynum", "processor", "auth", and "order_number".
+
 =cut
 
 sub approve {
   # to break up the Big Wall of Code that is import_results
   my $new = shift;
-  my $paybatch = shift;
+  my %opt = @_;
   my $paybatchnum = $new->paybatchnum;
   my $old = qsearchs('cust_pay_batch', { paybatchnum => $paybatchnum })
     or return "paybatchnum $paybatchnum not found";
@@ -317,13 +321,17 @@ sub approve {
   my $cust_pay = new FS::cust_pay ( {
       'custnum'   => $new->custnum,
       'payby'     => $new->payby,
-      'paybatch'  => $paybatch,
       'payinfo'   => $new->payinfo || $old->payinfo,
       'paid'      => $new->paid,
       '_date'     => $new->_date,
       'usernum'   => $new->usernum,
       'batchnum'  => $new->batchnum,
+      'gatewaynum'    => $opt{'gatewaynum'},
+      'processor'     => $opt{'processor'},
+      'auth'          => $opt{'auth'},
+      'order_number'  => $opt{'order_number'} 
     } );
+
   $error = $cust_pay->insert;
   if ( $error ) {
     return "error inserting payment for paybatchnum $paybatchnum: $error\n";
@@ -361,6 +369,12 @@ sub decline {
       # Void the payment
       my $cust_pay = qsearchs('cust_pay', { 
           custnum  => $new->custnum,
+          batchnum => $new->batchnum
+        });
+      # these should all be migrated over, but if it's not found, look for
+      # batchnum in the 'paybatch' field also
+      $cust_pay ||= qsearchs('cust_pay', { 
+          custnum  => $new->custnum,
           paybatch => $new->batchnum
         });
       if ( !$cust_pay ) {
@@ -375,6 +389,7 @@ sub decline {
     }
   } # !$old->status
   $new->status('Declined');
+  $new->error_message($reason);
   my $error = $new->replace($old);
   if ( $error ) {
     return "error updating status of paybatchnum $paybatchnum: $error\n";
index 22a7b2c..741d440 100644 (file)
@@ -1,12 +1,13 @@
 package FS::cust_pkg;
 
 use strict;
-use base qw( FS::otaker_Mixin FS::cust_main_Mixin FS::location_Mixin
+use base qw( FS::otaker_Mixin FS::cust_main_Mixin
+             FS::contact_Mixin FS::location_Mixin
              FS::m2m_Common FS::option_Common );
 use vars qw($disable_agentcheck $DEBUG $me);
 use Carp qw(cluck);
 use Scalar::Util qw( blessed );
-use List::Util qw(max);
+use List::Util qw(min max);
 use Tie::IxHash;
 use Time::Local qw( timelocal timelocal_nocheck );
 use MIME::Entity;
@@ -17,10 +18,13 @@ use FS::CurrentUser;
 use FS::cust_svc;
 use FS::part_pkg;
 use FS::cust_main;
+use FS::contact;
 use FS::cust_location;
 use FS::pkg_svc;
 use FS::cust_bill_pkg;
 use FS::cust_pkg_detail;
+use FS::cust_pkg_usage;
+use FS::cdr_cust_pkg_usage;
 use FS::cust_event;
 use FS::h_cust_svc;
 use FS::reg_code;
@@ -197,6 +201,15 @@ Previous locationnum
 
 =item waive_setup
 
+=item main_pkgnum
+
+The pkgnum of the package that this package is supplemental to, if any.
+
+=item pkglinknum
+
+The package link (L<FS::part_pkg_link>) that defines this supplemental
+package, if it is one.
+
 =back
 
 Note: setup, last_bill, bill, adjourn, susp, expire, cancel and change_date
@@ -214,7 +227,7 @@ Create a new billing item.  To add the item to the database, see L<"insert">.
 =cut
 
 sub table { 'cust_pkg'; }
-sub cust_linked { $_[0]->cust_main_custnum; } 
+sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum } 
 sub cust_unlinked_msg {
   my $self = shift;
   "WARNING: can't find cust_main.custnum ". $self->custnum.
@@ -256,6 +269,12 @@ a ticket will be added to this customer with this subject
 
 an optional queue name for ticket additions
 
+=item allow_pkgpart
+
+Don't check the legality of the package definition.  This should be used
+when performing a package change that doesn't change the pkgpart (i.e. 
+a location change).
+
 =back
 
 =cut
@@ -263,7 +282,8 @@ an optional queue name for ticket additions
 sub insert {
   my( $self, %options ) = @_;
 
-  my $error = $self->check_pkgpart;
+  my $error;
+  $error = $self->check_pkgpart unless $options{'allow_pkgpart'};
   return $error if $error;
 
   my $part_pkg = $self->part_pkg;
@@ -594,13 +614,15 @@ replace methods.
 sub check {
   my $self = shift;
 
-  $self->locationnum('') if !$self->locationnum || $self->locationnum == -1;
+  if ( !$self->locationnum or $self->locationnum == -1 ) {
+    $self->set('locationnum', $self->cust_main->ship_locationnum);
+  }
 
   my $error = 
     $self->ut_numbern('pkgnum')
     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
     || $self->ut_numbern('pkgpart')
-    || $self->check_pkgpart
+    || $self->ut_foreign_keyn('contactnum',  'contact',       'contactnum' )
     || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
     || $self->ut_numbern('start_date')
     || $self->ut_numbern('setup')
@@ -616,6 +638,8 @@ sub check {
     || $self->ut_numbern('agent_pkgid')
     || $self->ut_enum('recur_show_zero', [ '', 'Y', 'N', ])
     || $self->ut_enum('setup_show_zero', [ '', 'Y', 'N', ])
+    || $self->ut_foreign_keyn('main_pkgnum', 'cust_pkg', 'pkgnum')
+    || $self->ut_foreign_keyn('pkglinknum', 'part_pkg_link', 'pkglinknum')
   ;
   return $error if $error;
 
@@ -639,14 +663,19 @@ sub check {
 
 =item check_pkgpart
 
+Check the pkgpart to make sure it's allowed with the reg_code and/or
+promo_code of the package (if present) and with the customer's agent.
+Called from C<insert>, unless we are doing a package change that doesn't
+affect pkgpart.
+
 =cut
 
 sub check_pkgpart {
   my $self = shift;
 
-  my $error = $self->ut_numbern('pkgpart');
-  return $error if $error;
+  # my $error = $self->ut_numbern('pkgpart'); # already done
 
+  my $error;
   if ( $self->reg_code ) {
 
     unless ( grep { $self->pkgpart == $_->pkgpart }
@@ -730,6 +759,11 @@ sub cancel {
   my( $self, %options ) = @_;
   my $error;
 
+  # pass all suspend/cancel actions to the main package
+  if ( $self->main_pkgnum and !$options{'from_main'} ) {
+    return $self->main_pkg->cancel(%options);
+  }
+
   my $conf = new FS::Conf;
 
   warn "cust_pkg::cancel called with options".
@@ -835,6 +869,22 @@ sub cancel {
     return $error;
   }
 
+  foreach my $supp_pkg ( $self->supplemental_pkgs ) {
+    $error = $supp_pkg->cancel(%options, 'from_main' => 1);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "canceling supplemental pkg#".$supp_pkg->pkgnum.": $error";
+    }
+  }
+
+  foreach my $usage ( $self->cust_pkg_usage ) {
+    $error = $usage->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "deleting usage pools: $error";
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   return '' if $date; #no errors
 
@@ -894,6 +944,9 @@ svc_fatal: service provisioning errors are fatal
 
 svc_errors: pass an array reference, will be filled in with any provisioning errors
 
+main_pkgnum: link the package as a supplemental package of this one.  For 
+internal use only.
+
 =cut
 
 sub uncancel {
@@ -902,6 +955,10 @@ sub uncancel {
   #in case you try do do $uncancel-date = $cust_pkg->uncacel 
   return '' unless $self->get('cancel');
 
+  if ( $self->main_pkgnum and !$options{'main_pkgnum'} ) {
+    return $self->main_pkg->uncancel(%options);
+  }
+
   ##
   # Transaction-alize
   ##
@@ -926,6 +983,7 @@ sub uncancel {
     bill            => ( $options{'bill'}      || $self->get('bill')      ),
     uncancel        => time,
     uncancel_pkgnum => $self->pkgnum,
+    main_pkgnum     => ($options{'main_pkgnum'} || ''),
     map { $_ => $self->get($_) } qw(
       custnum pkgpart locationnum
       setup
@@ -937,6 +995,7 @@ sub uncancel {
 
   my $error = $cust_pkg->insert(
     'change' => 1, #supresses any referral credit to a referring customer
+    'allow_pkgpart' => 1, # allow this even if the package def is disabled
   );
   if ($error) {
     $dbh->rollback if $oldAutoCommit;
@@ -1023,6 +1082,20 @@ sub uncancel {
   }
 
   ##
+  # Uncancel any supplemental packages, and make them supplemental to the 
+  # new one.
+  ##
+
+  foreach my $supp_pkg ( $self->supplemental_pkgs ) {
+    my $new_pkg;
+    $error = $supp_pkg->uncancel(%options, 'main_pkgnum' => $cust_pkg->pkgnum);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "canceling supplemental pkg#".$supp_pkg->pkgnum.": $error";
+    }
+  }
+
+  ##
   # Finish
   ##
 
@@ -1111,6 +1184,9 @@ of final invoices or unused-time credits
 unsuspended.  This may be more convenient than calling C<unsuspend()>
 separately.
 
+=item from_main - allows a supplemental package to be suspended, rather
+than redirecting the method call to its main package.  For internal use.
+
 =back
 
 If there is an error, returns the error, otherwise returns false.
@@ -1121,6 +1197,11 @@ sub suspend {
   my( $self, %options ) = @_;
   my $error;
 
+  # pass all suspend/cancel actions to the main package
+  if ( $self->main_pkgnum and !$options{'from_main'} ) {
+    return $self->main_pkg->suspend(%options);
+  }
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE'; 
@@ -1271,6 +1352,14 @@ sub suspend {
 
   }
 
+  foreach my $supp_pkg ( $self->supplemental_pkgs ) {
+    $error = $supp_pkg->suspend(%options, 'from_main' => 1);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "suspending supplemental pkg#".$supp_pkg->pkgnum.": $error";
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   ''; #no errors
@@ -1353,6 +1442,11 @@ sub unsuspend {
   my( $self, %opt ) = @_;
   my $error;
 
+  # pass all suspend/cancel actions to the main package
+  if ( $self->main_pkgnum and !$opt{'from_main'} ) {
+    return $self->main_pkg->unsuspend(%opt);
+  }
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE'; 
@@ -1511,6 +1605,14 @@ sub unsuspend {
 
   }
 
+  foreach my $supp_pkg ( $self->supplemental_pkgs ) {
+    $error = $supp_pkg->unsuspend(%opt, 'from_main' => 1);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "unsuspending supplemental pkg#".$supp_pkg->pkgnum.": $error";
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   ''; #no errors
@@ -1662,12 +1764,23 @@ sub change {
 
   if ( $opt->{'cust_location'} &&
        ( ! $opt->{'locationnum'} || $opt->{'locationnum'} == -1 ) ) {
-    $error = $opt->{'cust_location'}->insert;
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return "inserting cust_location (transaction rolled back): $error";
+
+    if ( ! $opt->{'cust_location'}->locationnum ) {
+      # not inserted yet
+      $error = $opt->{'cust_location'}->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "inserting cust_location (transaction rolled back): $error";
+      }
     }
     $opt->{'locationnum'} = $opt->{'cust_location'}->locationnum;
+
+  }
+
+  # whether to override pkgpart checking on the new package
+  my $same_pkgpart = 1;
+  if ( $opt->{'pkgpart'} and ( $opt->{'pkgpart'} != $self->pkgpart ) ) {
+    $same_pkgpart = 0;
   }
 
   my $unused_credit = 0;
@@ -1676,9 +1789,11 @@ sub change {
   # going to be credited for remaining time, don't keep setup, bill, 
   # or last_bill dates, and DO pass the flag to cancel() to credit 
   # the customer.
-  if ( $opt->{'pkgpart'} and $opt->{'pkgpart'} != $self->pkgpart ) {
+  if ( $opt->{'pkgpart'} 
+       and $opt->{'pkgpart'} != $self->pkgpart
+       and $self->part_pkg->option('unused_credit_change', 1) ) {
+    $unused_credit = 1;
     $keep_dates = 0;
-    $unused_credit = 1 if $self->part_pkg->option('unused_credit_change', 1);
     $hash{$_} = '' foreach qw(setup bill last_bill);
   }
 
@@ -1692,6 +1807,12 @@ sub change {
   # (i.e. customer default location)
   $opt->{'locationnum'} = $self->locationnum if !exists($opt->{'locationnum'});
 
+  # usually this doesn't matter.  the two cases where it does are:
+  # 1. unused_credit_change + pkgpart change + setup fee on the new package
+  # and
+  # 2. (more importantly) changing a package before it's billed
+  $hash{'waive_setup'} = $self->waive_setup;
+
   # Create the new package.
   my $cust_pkg = new FS::cust_pkg {
     custnum      => $self->custnum,
@@ -1700,8 +1821,8 @@ sub change {
     locationnum  => ( $opt->{'locationnum'}                        ),
     %hash,
   };
-
-  $error = $cust_pkg->insert( 'change' => 1 );
+  $error = $cust_pkg->insert( 'change' => 1,
+                              'allow_pkgpart' => $same_pkgpart );
   if ($error) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -1747,6 +1868,84 @@ sub change {
       $dbh->rollback if $oldAutoCommit;
       return "Error setting usage values: $error";
     }
+  } else {
+    # if NOT changing pkgpart, transfer any usage pools over
+    foreach my $usage ($self->cust_pkg_usage) {
+      $usage->set('pkgnum', $cust_pkg->pkgnum);
+      $error = $usage->replace;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "Error transferring usage pools: $error";
+      }
+    }
+  }
+
+  # transfer discounts, if we're not changing pkgpart
+  if ( $same_pkgpart ) {
+    foreach my $old_discount ($self->cust_pkg_discount_active) {
+      # don't remove the old discount, we may still need to bill that package.
+      my $new_discount = new FS::cust_pkg_discount {
+        'pkgnum'      => $cust_pkg->pkgnum,
+        'discountnum' => $old_discount->discountnum,
+        'months_used' => $old_discount->months_used,
+      };
+      $error = $new_discount->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "Error transferring discounts: $error";
+      }
+    }
+  }
+
+  # Order any supplemental packages.
+  my $part_pkg = $cust_pkg->part_pkg;
+  my @old_supp_pkgs = $self->supplemental_pkgs;
+  my @new_supp_pkgs;
+  foreach my $link ($part_pkg->supp_part_pkg_link) {
+    my $old;
+    foreach (@old_supp_pkgs) {
+      if ($_->pkgpart == $link->dst_pkgpart) {
+        $old = $_;
+        $_->pkgpart(0); # so that it can't match more than once
+      }
+      last if $old;
+    }
+    # false laziness with FS::cust_main::Packages::order_pkg
+    my $new = FS::cust_pkg->new({
+        pkgpart       => $link->dst_pkgpart,
+        pkglinknum    => $link->pkglinknum,
+        custnum       => $self->custnum,
+        main_pkgnum   => $cust_pkg->pkgnum,
+        locationnum   => $cust_pkg->locationnum,
+        start_date    => $cust_pkg->start_date,
+        order_date    => $cust_pkg->order_date,
+        expire        => $cust_pkg->expire,
+        adjourn       => $cust_pkg->adjourn,
+        contract_end  => $cust_pkg->contract_end,
+        refnum        => $cust_pkg->refnum,
+        discountnum   => $cust_pkg->discountnum,
+        waive_setup   => $cust_pkg->waive_setup,
+    });
+    if ( $old and $opt->{'keep_dates'} ) {
+      foreach (qw(setup bill last_bill)) {
+        $new->set($_, $old->get($_));
+      }
+    }
+    $error = $new->insert( allow_pkgpart => $same_pkgpart );
+    # transfer services
+    if ( $old ) {
+      $error ||= $old->transfer($new);
+    }
+    if ( $error and $error > 0 ) {
+      # no reason why this should ever fail, but still...
+      $error = "Unable to transfer all services from supplemental package ".
+        $old->pkgnum;
+    }
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+    push @new_supp_pkgs, $new;
   }
 
   #Good to go, cancel old package.  Notify 'cancel' of whether to credit 
@@ -1754,6 +1953,7 @@ sub change {
   #Don't allow billing the package (preceding period packages and/or 
   #outstanding usage) if we are keeping dates (i.e. location changing), 
   #because the new package will be billed for the same date range.
+  #Supplemental packages are also canceled here.
   $error = $self->cancel(
     quiet         => 1, 
     unused_credit => $unused_credit,
@@ -1766,7 +1966,9 @@ sub change {
 
   if ( $conf->exists('cust_pkg-change_pkgpart-bill_now') ) {
     #$self->cust_main
-    my $error = $cust_pkg->cust_main->bill( 'pkg_list' => [ $cust_pkg ] );
+    my $error = $cust_pkg->cust_main->bill( 
+      'pkg_list' => [ $cust_pkg, @new_supp_pkgs ]
+    );
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
@@ -1779,6 +1981,24 @@ sub change {
 
 }
 
+=item set_quantity QUANTITY
+
+Change the package's quantity field.  This is the one package property
+that can safely be changed without canceling and reordering the package
+(because it doesn't affect tax eligibility).  Returns an error or an 
+empty string.
+
+=cut
+
+sub set_quantity {
+  my $self = shift;
+  $self = $self->replace_old; # just to make sure
+  my $qty = shift;
+  ($qty =~ /^\d+$/ and $qty > 0) or return "bad package quantity $qty";
+  $self->set('quantity' => $qty);
+  $self->replace;
+}
+
 use Storable 'thaw';
 use MIME::Base64;
 sub process_bulk_cust_pkg {
@@ -2469,7 +2689,7 @@ sub statuscolor {
 =item pkg_label
 
 Returns a label for this package.  (Currently "pkgnum: pkg - comment" or
-"pkg-comment" depending on user preference).
+"pkg - comment" depending on user preference).
 
 =cut
 
@@ -2496,6 +2716,17 @@ sub pkg_label_long {
   $label;
 }
 
+=item pkg_locale
+
+Returns a customer-localized label for this package.
+
+=cut
+
+sub pkg_locale {
+  my $self = shift;
+  $self->part_pkg->pkg_locale( $self->cust_main->locale );
+}
+
 =item primary_cust_svc
 
 Returns a primary service (as FS::cust_svc object) if one can be identified.
@@ -3137,6 +3368,207 @@ sub cust_pkg_discount_active {
   grep { $_->status eq 'active' } $self->cust_pkg_discount;
 }
 
+=item cust_pkg_usage
+
+Returns a list of all voice usage counters attached to this package.
+
+=cut
+
+sub cust_pkg_usage {
+  my $self = shift;
+  qsearch('cust_pkg_usage', { pkgnum => $self->pkgnum });
+}
+
+=item apply_usage OPTIONS
+
+Takes the following options:
+- cdr: a call detail record (L<FS::cdr>)
+- rate_detail: the rate determined for this call (L<FS::rate_detail>)
+- minutes: the maximum number of minutes to be charged
+
+Finds available usage minutes for a call of this class, and subtracts
+up to that many minutes from the usage pool.  If the usage pool is empty,
+and the C<cdr-minutes_priority> global config option is set, minutes may
+be taken from other calls as well.  Either way, an allocation record will
+be created (L<FS::cdr_cust_pkg_usage>) and this method will return the 
+number of minutes of usage applied to the call.
+
+=cut
+
+sub apply_usage {
+  my ($self, %opt) = @_;
+  my $cdr = $opt{cdr};
+  my $rate_detail = $opt{rate_detail};
+  my $minutes = $opt{minutes};
+  my $classnum = $rate_detail->classnum;
+  my $pkgnum = $self->pkgnum;
+  my $custnum = $self->custnum;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE'; 
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE'; 
+  local $SIG{PIPE} = 'IGNORE'; 
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+  my $order = FS::Conf->new->config('cdr-minutes_priority');
+
+  my $is_classnum;
+  if ( $classnum ) {
+    $is_classnum = ' part_pkg_usage_class.classnum = '.$classnum;
+  } else {
+    $is_classnum = ' part_pkg_usage_class.classnum IS NULL';
+  }
+  my @usage_recs = qsearch({
+      'table'     => 'cust_pkg_usage',
+      'addl_from' => ' JOIN part_pkg_usage       USING (pkgusagepart)'.
+                     ' JOIN cust_pkg             USING (pkgnum)'.
+                     ' JOIN part_pkg_usage_class USING (pkgusagepart)',
+      'select'    => 'cust_pkg_usage.*',
+      'extra_sql' => " WHERE ( cust_pkg.pkgnum = $pkgnum OR ".
+                     " ( cust_pkg.custnum = $custnum AND ".
+                     " part_pkg_usage.shared IS NOT NULL ) ) AND ".
+                     $is_classnum . ' AND '.
+                     " cust_pkg_usage.minutes > 0",
+      'order_by'  => " ORDER BY priority ASC",
+  });
+
+  my $orig_minutes = $minutes;
+  my $error;
+  while (!$error and $minutes > 0 and @usage_recs) {
+    my $cust_pkg_usage = shift @usage_recs;
+    $cust_pkg_usage->select_for_update;
+    my $cdr_cust_pkg_usage = FS::cdr_cust_pkg_usage->new({
+        pkgusagenum => $cust_pkg_usage->pkgusagenum,
+        acctid      => $cdr->acctid,
+        minutes     => min($cust_pkg_usage->minutes, $minutes),
+    });
+    $cust_pkg_usage->set('minutes',
+      sprintf('%.0f', $cust_pkg_usage->minutes - $cdr_cust_pkg_usage->minutes)
+    );
+    $error = $cust_pkg_usage->replace || $cdr_cust_pkg_usage->insert;
+    $minutes -= $cdr_cust_pkg_usage->minutes;
+  }
+  if ( $order and $minutes > 0 and !$error ) {
+    # then try to steal minutes from another call
+    my %search = (
+        'table'     => 'cdr_cust_pkg_usage',
+        'addl_from' => ' JOIN cust_pkg_usage        USING (pkgusagenum)'.
+                       ' JOIN part_pkg_usage        USING (pkgusagepart)'.
+                       ' JOIN cust_pkg              USING (pkgnum)'.
+                       ' JOIN part_pkg_usage_class  USING (pkgusagepart)'.
+                       ' JOIN cdr                   USING (acctid)',
+        'select'    => 'cdr_cust_pkg_usage.*',
+        'extra_sql' => " WHERE cdr.freesidestatus = 'rated' AND ".
+                       " ( cust_pkg.pkgnum = $pkgnum OR ".
+                       " ( cust_pkg.custnum = $custnum AND ".
+                       " part_pkg_usage.shared IS NOT NULL ) ) AND ".
+                       " part_pkg_usage_class.classnum = $classnum",
+        'order_by'  => ' ORDER BY part_pkg_usage.priority ASC',
+    );
+    if ( $order eq 'time' ) {
+      # find CDRs that are using minutes, but have a later startdate
+      # than this call
+      my $startdate = $cdr->startdate;
+      if ($startdate !~ /^\d+$/) {
+        die "bad cdr startdate '$startdate'";
+      }
+      $search{'extra_sql'} .= " AND cdr.startdate > $startdate";
+      # minimize needless reshuffling
+      $search{'order_by'} .= ', cdr.startdate DESC';
+    } else {
+      # XXX may not work correctly with rate_time schedules.  Could 
+      # fix this by storing ratedetailnum in cdr_cust_pkg_usage, I 
+      # think...
+      $search{'addl_from'} .=
+        ' JOIN rate_detail'.
+        ' ON (cdr.rated_ratedetailnum = rate_detail.ratedetailnum)';
+      if ( $order eq 'rate_high' ) {
+        $search{'extra_sql'} .= ' AND rate_detail.min_charge < '.
+                                $rate_detail->min_charge;
+        $search{'order_by'} .= ', rate_detail.min_charge ASC';
+      } elsif ( $order eq 'rate_low' ) {
+        $search{'extra_sql'} .= ' AND rate_detail.min_charge > '.
+                                $rate_detail->min_charge;
+        $search{'order_by'} .= ', rate_detail.min_charge DESC';
+      } else {
+        #  this should really never happen
+        die "invalid cdr-minutes_priority value '$order'\n";
+      }
+    }
+    my @cdr_usage_recs = qsearch(\%search);
+    my %reproc_cdrs;
+    while (!$error and @cdr_usage_recs and $minutes > 0) {
+      my $cdr_cust_pkg_usage = shift @cdr_usage_recs;
+      my $cust_pkg_usage = $cdr_cust_pkg_usage->cust_pkg_usage;
+      my $old_cdr = $cdr_cust_pkg_usage->cdr;
+      $reproc_cdrs{$old_cdr->acctid} = $old_cdr;
+      $cdr_cust_pkg_usage->select_for_update;
+      $old_cdr->select_for_update;
+      $cust_pkg_usage->select_for_update;
+      # in case someone else stole the usage from this CDR
+      # while waiting for the lock...
+      next if $old_cdr->acctid != $cdr_cust_pkg_usage->acctid;
+      # steal the usage allocation and flag the old CDR for reprocessing
+      $cdr_cust_pkg_usage->set('acctid', $cdr->acctid);
+      # if the allocation is more minutes than we need, adjust it...
+      my $delta = $cdr_cust_pkg_usage->minutes - $minutes;
+      if ( $delta > 0 ) {
+        $cdr_cust_pkg_usage->set('minutes', $minutes);
+        $cust_pkg_usage->set('minutes', $cust_pkg_usage->minutes + $delta);
+        $error = $cust_pkg_usage->replace;
+      }
+      #warn 'CDR '.$cdr->acctid . ' stealing allocation '.$cdr_cust_pkg_usage->cdrusagenum.' from CDR '.$old_cdr->acctid."\n";
+      $error ||= $cdr_cust_pkg_usage->replace;
+      # deduct the stolen minutes
+      $minutes -= $cdr_cust_pkg_usage->minutes;
+    }
+    # after all minute-stealing is done, reset the affected CDRs
+    foreach (values %reproc_cdrs) {
+      $error ||= $_->set_status('');
+      # XXX or should we just call $cdr->rate right here?
+      # it's not like we can create a loop this way, since the min_charge
+      # or call time has to go monotonically in one direction.
+      # we COULD get some very deep recursions going, though...
+    }
+  } # if $order and $minutes
+  if ( $error ) {
+    $dbh->rollback;
+    die "error applying included minutes\npkgnum ".$self->pkgnum.", class $classnum, acctid ".$cdr->acctid."\n$error\n"
+  } else {
+    $dbh->commit if $oldAutoCommit;
+    return $orig_minutes - $minutes;
+  }
+}
+
+=item supplemental_pkgs
+
+Returns a list of all packages supplemental to this one.
+
+=cut
+
+sub supplemental_pkgs {
+  my $self = shift;
+  qsearch('cust_pkg', { 'main_pkgnum' => $self->pkgnum });
+}
+
+=item main_pkg
+
+Returns the package that this one is supplemental to, if any.
+
+=cut
+
+sub main_pkg {
+  my $self = shift;
+  if ( $self->main_pkgnum ) {
+    return FS::cust_pkg->by_key($self->main_pkgnum);
+  }
+  return;
+}
+
 =back
 
 =head1 CLASS METHODS
@@ -3664,10 +4096,10 @@ sub search {
 
   my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
 
-  my $addl_from = 'LEFT JOIN cust_main USING ( custnum  ) '.
-                  'LEFT JOIN part_pkg  USING ( pkgpart  ) '.
+  my $addl_from = 'LEFT JOIN part_pkg  USING ( pkgpart  ) '.
                   'LEFT JOIN pkg_class ON ( part_pkg.classnum = pkg_class.classnum ) '.
-                  'LEFT JOIN cust_location USING ( locationnum ) ';
+                  'LEFT JOIN cust_location USING ( locationnum ) '.
+                  FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
 
   my $select;
   my $count_query;
@@ -3951,11 +4383,25 @@ sub order {
                                       %hash,
                                     };
     $error = $cust_pkg->insert( 'change' => $change );
+    push @$return_cust_pkg, $cust_pkg;
+
+    foreach my $link ($cust_pkg->part_pkg->supp_part_pkg_link) {
+      my $supp_pkg = FS::cust_pkg->new({
+          custnum => $custnum,
+          pkgpart => $link->dst_pkgpart,
+          refnum  => $refnum,
+          main_pkgnum => $cust_pkg->pkgnum,
+          %hash,
+      });
+      $error ||= $supp_pkg->insert( 'change' => $change );
+      push @$return_cust_pkg, $supp_pkg;
+    }
+
     if ($error) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
     }
-    push @$return_cust_pkg, $cust_pkg;
+
   }
   # $return_cust_pkg now contains refs to all of the newly 
   # created packages.
index 5f4d0dc..d82d949 100644 (file)
@@ -164,7 +164,7 @@ sub check {
     $self->ut_numbern('pkgdiscountnum')
     || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum')
     || $self->ut_foreign_key('discountnum', 'discount', 'discountnum' )
-    || $self->ut_float('months_used') #actually decimal, but this will do
+    || $self->ut_sfloat('months_used') #actually decimal, but this will do
     || $self->ut_numbern('end_date')
     || $self->ut_alphan('otaker')
     || $self->ut_numbern('usernum')
@@ -202,7 +202,7 @@ sub discount {
   qsearchs('discount', { 'discountnum' => $self->discountnum } );
 }
 
-=item increment_months_used
+=item increment_months_used MONTHS
 
 Increments months_used by the given parameter
 
@@ -216,6 +216,31 @@ sub increment_months_used {
   $self->replace();
 }
 
+=item decrement_months_used MONTHS
+
+Decrement months_used by the given parameter
+
+(Note: as in, extending the length of the discount.  Typically only used to
+stack/extend a discount when the customer package has one active already.)
+
+=cut
+
+sub decrement_months_used {
+  my( $self, $recharged ) = @_;
+  #UPDATE cust_pkg_discount SET months_used = months_used - ?
+  #leaves no history, and billing is mutexed per-customer
+
+  #we're run from part_event/Action/referral_pkg_discount on behalf of a
+  # different customer, so we need to grab this customer's mutex.
+  #   incidentally, that's some inelegant encapsulation breaking shit, and a
+  #   great argument in favor of native-DB trigger history so we can trust
+  #   in normal ACID like the SQL above instead of this
+  $self->cust_pkg->cust_main->select_for_update;
+
+  $self->months_used( $self->months_used - $recharged );
+  $self->replace();
+}
+
 =item status
 
 =cut
diff --git a/FS/FS/cust_pkg_usage.pm b/FS/FS/cust_pkg_usage.pm
new file mode 100644 (file)
index 0000000..0eefd74
--- /dev/null
@@ -0,0 +1,163 @@
+package FS::cust_pkg_usage;
+
+use strict;
+use base qw( FS::Record );
+use FS::cust_pkg;
+use FS::part_pkg_usage;
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::cust_pkg_usage - Object methods for cust_pkg_usage records
+
+=head1 SYNOPSIS
+
+  use FS::cust_pkg_usage;
+
+  $record = new FS::cust_pkg_usage \%hash;
+  $record = new FS::cust_pkg_usage { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_pkg_usage object represents a counter of remaining included
+minutes on a voice-call package.  FS::cust_pkg_usage inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item pkgusagenum - primary key
+
+=item pkgnum - the package (L<FS::cust_pkg>) containing the usage
+
+=item pkgusagepart - the usage stock definition (L<FS::part_pkg_usage>).
+This record in turn links to the call usage classes that are eligible to 
+use these minutes.
+
+=item minutes - the remaining minutes
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+=cut
+
+sub table { 'cust_pkg_usage'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+sub delete {
+  my $self = shift;
+  my $error = $self->reset || $self->SUPER::delete;
+}
+
+=item reset
+
+Remove all allocations of this usage to CDRs.
+
+=cut
+
+sub reset {
+  my $self = shift;
+  my $error = '';
+  foreach (qsearch('cdr_cust_pkg_usage', { pkgusagenum => $self->pkgusagenum }))
+  {
+    $error ||= $_->delete;
+  }
+  $error;
+}
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('pkgusagenum')
+    || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum')
+    || $self->ut_numbern('minutes')
+    || $self->ut_foreign_key('pkgusagepart', 'part_pkg_usage', 'pkgusagepart')
+  ;
+  return $error if $error;
+
+  if ( $self->minutes eq '' ) {
+    $self->set(minutes => $self->part_pkg_usage->minutes);
+  }
+
+  $self->SUPER::check;
+}
+
+=item cust_pkg
+
+Return the L<FS::cust_pkg> linked to this record.
+
+=item part_pkg_usage
+
+Return the L<FS::part_pkg_usage> linked to this record.
+
+=cut
+
+sub cust_pkg {
+  my $self = shift;
+  FS::cust_pkg->by_key($self->pkgnum);
+}
+
+sub part_pkg_usage {
+  my $self = shift;
+  FS::part_pkg_usage->by_key($self->pkgusagepart);
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index b608b23..1653840 100644 (file)
@@ -13,6 +13,7 @@ use FS::pkg_svc;
 use FS::domain_record;
 use FS::part_export;
 use FS::cdr;
+use FS::UI::Web;
 
 #most FS::svc_ classes are autoloaded in svc_x emthod
 use FS::svc_acct;  #this one is used in the cache stuff
@@ -883,7 +884,7 @@ sub smart_search_param {
   my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
   #for agentnum
   my $addl_from = ' LEFT JOIN cust_pkg  USING ( pkgnum  )'.
-                  ' LEFT JOIN cust_main USING ( custnum )'.
+                  FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
                   ' LEFT JOIN part_svc  USING ( svcpart )';
 
   (
@@ -894,6 +895,48 @@ sub smart_search_param {
   );
 }
 
+sub _upgrade_data {
+  my $class = shift;
+
+  # fix missing (deleted by mistake) svc_x records
+  warn "searching for missing svc_x records...\n";
+  my %search = (
+    'table'     => 'cust_svc',
+    'select'    => 'cust_svc.*',
+    'addl_from' => ' LEFT JOIN ( ' .
+      join(' UNION ',
+        map { "SELECT svcnum FROM $_" } 
+        FS::part_svc->svc_tables
+      ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
+    'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
+  );
+  my @svcs = qsearch(\%search);
+  warn "found ".scalar(@svcs)."\n";
+
+  local $FS::Record::nowarn_classload = 1; # for h_svc_
+  local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
+
+  my %h_search = (
+    'hashref'  => { history_action => 'delete' },
+    'order_by' => ' ORDER BY history_date DESC LIMIT 1',
+  );
+  foreach my $cust_svc (@svcs) {
+    my $svcnum = $cust_svc->svcnum;
+    my $svcdb = $cust_svc->part_svc->svcdb;
+    $h_search{'hashref'}{'svcnum'} = $svcnum;
+    $h_search{'table'} = "h_$svcdb";
+    my $h_svc_x = qsearchs(\%h_search)
+      or next;
+    my $class = "FS::$svcdb";
+    my $new_svc_x = $class->new({ $h_svc_x->hash });
+    my $error = $new_svc_x->insert;
+    warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
+      if $error;
+  }
+
+  '';
+}
+
 =back
 
 =head1 BUGS
index 0370f5f..b08f8f7 100644 (file)
@@ -5,6 +5,7 @@ use vars qw( @ISA );
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::part_export;
 use FS::part_svc;
+use FS::svc_export_machine;
 
 @ISA = qw(FS::Record);
 
@@ -209,6 +210,19 @@ sub insert {
   } #end of duplicate check, whew
 
   $error = $self->SUPER::insert;
+
+  my $part_export = $self->part_export;
+  if ( !$error and $part_export->default_machine ) {
+    foreach my $cust_svc ( $self->part_svc->cust_svc ) {
+      my $svc_export_machine = FS::svc_export_machine->new({
+          'exportnum'   => $self->exportnum,
+          'svcnum'      => $cust_svc->svcnum,
+          'machinenum'  => $part_export->default_machine,
+      });
+      $error ||= $svc_export_machine->insert;
+    }
+  }
+
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -251,7 +265,23 @@ Delete this record from the database.
 
 =cut
 
-# the delete method can be inherited from FS::Record
+sub delete {
+  my $self = shift;
+  my $dbh = dbh;
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+
+  my $error = $self->SUPER::delete;
+  foreach ($self->svc_export_machine) {
+    $error ||= $_->delete;
+  }
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+}
+
 
 =item replace OLD_RECORD
 
@@ -307,6 +337,24 @@ sub part_svc {
   qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
 }
 
+=item svc_export_machine
+
+Returns all export hostname records (L<FS::svc_export_machine>) for this
+combination of svcpart and exportnum.
+
+=cut
+
+sub svc_export_machine {
+  my $self = shift;
+  qsearch({
+    'table'     => 'svc_export_machine',
+    'select'    => 'svc_export_machine.*',
+    'addl_from' => 'JOIN cust_svc USING (svcnum)',
+    'hashref'   => { 'exportnum' => $self->exportnum },
+    'extra_sql' => ' AND cust_svc.svcpart = '.$self->svcpart,
+  });
+}
+
 =back
 
 =head1 BUGS
index e38346a..2f5e476 100644 (file)
@@ -3,7 +3,7 @@ package FS::msg_template;
 use strict;
 use base qw( FS::Record );
 use Text::Template;
-use FS::Misc qw( generate_email send_email );
+use FS::Misc qw( generate_email send_email do_print );
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs );
 use FS::UID qw( dbh );
@@ -457,24 +457,13 @@ sub render {
   my %hash = $self->prepare(%opt);
   my $html = $hash{'html_body'};
 
-  my $tmp = 'msg'.$self->msgnum.'-'.time2str('%Y%m%d', time).'-XXXXXXXX';
-  my $dir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc";
-
   # Graphics/stylesheets should probably go in /var/www on the Freeside 
   # machine.
   my $kit = PDF::WebKit->new(\$html); #%options
   # hack to use our wrapper script
   $kit->configure(sub { shift->wkhtmltopdf('freeside-wkhtmltopdf') });
-  my $fh = File::Temp->new(
-    TEMPLATE  => $tmp,
-    DIR       => $dir,
-    UNLINK    => 0,
-    SUFFIX    => '.pdf'
-  );
 
-  print $fh $kit->to_pdf;
-  close $fh;
-  return $fh->filename;
+  $kit->to_pdf;
 }
 
 =item print OPTIONS
@@ -484,13 +473,10 @@ Render a PDF and send it to the printer.  OPTIONS are as for 'render'.
 =cut
 
 sub print {
-  my $file = render(@_);
-  my @lpr = $conf->config('lpr');
-  run ([@lpr, '-r'], '<', $file)
-    or die "lpr error:\n$?\n";
+  my( $self, %opt ) = @_;
+  do_print( [ $self->render(%opt) ], agentnum=>$opt{cust_main}->agentnum );
 }
 
-
 # helper sub for package dates
 my $ymd = sub { $_[0] ? time2str('%Y-%m-%d', $_[0]) : '' };
 
index 73d32e0..33aeadd 100644 (file)
@@ -2,6 +2,7 @@ package FS::part_event::Action::Mixin::credit_agent_pkg_class;
 use base qw( FS::part_event::Action::Mixin::credit_pkg );
 
 use strict;
+use FS::Record qw(qsearchs);
 
 sub option_fields {
   my $class = shift;
index 9dcd701..a3c1d6e 100644 (file)
@@ -16,18 +16,24 @@ sub option_fields {
                      'type'    => 'input-percentage',
                      'default' => '100',
                    },
-    'what' => { 'label'   => 'Of',
-                'type'    => 'select',
-                #add additional ways to specify in the package def
-                'options' => [ qw( base_recur_permonth unit_setup recur_cost_permonth setup_cost ) ],
-                'labels'  => { 'base_recur_permonth' => 'Base monthly fee',
-                               'unit_setup'          => 'Setup fee',
-                               'recur_cost_permonth' => 'Monthly cost',
-                               'setup_cost'          => 'Setup cost',
-                             },
-              },
+    'what' => {
+      'label'   => 'Of',
+      'type'    => 'select',
+      #add additional ways to specify in the package def
+      'options' => [qw(
+        base_recur_permonth cust_bill_pkg_recur recur_cost_permonth
+        unit_setup setup_cost
+      )],
+      'labels'  => {
+        'base_recur_permonth' => 'Base monthly fee',
+        'cust_bill_pkg_recur' => 'Actual invoiced amount of most recent'.
+                                 ' recurring charge',
+        'recur_cost_permonth' => 'Monthly cost',
+        'unit_setup'          => 'Setup fee',
+        'setup_cost'          => 'Setup cost',
+      },
+    },
   );
-
 }
 
 #my %no_cust_pkg = ( 'setup_cost' => 1 );
index 2ba8136..073bb8f 100644 (file)
@@ -11,9 +11,10 @@ sub eventtable_hashref {
 
 sub option_fields {
   (
-    'notice_name'  => 'Reminder name',
-    #'notes' => { 'label' => 'Reminder notes' }, 
+    'notice_name' => 'Reminder name',
+    #'notes'      => { 'label' => 'Reminder notes' }, 
     #include standard notes?  no/prepend/append
+    'lpr'         => 'Optional alternate print command',
   );
 }
 
@@ -25,7 +26,10 @@ sub do_action {
   #my $cust_main = $self->cust_main($cust_bill);
   #my $cust_main = $cust_bill->cust_main;
 
-  $cust_bill->send({ 'notice_name' => $self->option('notice_name') });
+  $cust_bill->send({
+    'notice_name' => $self->option('notice_name'),
+    'lpr'         => $self->option('lpr'),
+  });
 }
 
 1;
diff --git a/FS/FS/part_event/Action/referral_pkg_billdate.pm b/FS/FS/part_event/Action/referral_pkg_billdate.pm
new file mode 100644 (file)
index 0000000..6b485e5
--- /dev/null
@@ -0,0 +1,59 @@
+package FS::part_event::Action::referral_pkg_billdate;
+
+use strict;
+use base qw( FS::part_event::Action );
+
+sub description { "Increment the referring customer's package's next bill date"; }
+
+#sub eventtable_hashref {
+#}
+
+sub option_fields {
+  (
+    'if_pkgpart' => { 'label'    => 'Only packages',
+                      'type'     => 'select-part_pkg',
+                      'multiple' => 1,
+                    },
+    'increment'  => { 'label'    => 'Increment by',
+                      'type'     => 'freq',
+                      'value'    => '1m',
+                    },
+  );
+}
+
+#false laziness w/referral_pkg_discount, probably should make
+# Mixin/referral_pkg.pm if we need changes or anything else in this vein
+sub do_action {
+  my( $self, $cust_object, $cust_event ) = @_;
+
+  my $cust_main = $self->cust_main($cust_object);
+
+  return 'No referring customer' unless $cust_main->referral_custnum;
+
+  my $referring_cust_main = $cust_main->referring_cust_main;
+  #return 'Referring customer is cancelled'
+  #  if $referring_cust_main->status eq 'cancelled';
+
+  my %if_pkgpart = map { $_=>1 } split(/\s*,\s*/, $self->option('if_pkgpart') );
+  my @cust_pkg = grep $if_pkgpart{ $_->pkgpart },
+                      $referring_cust_main->billing_pkgs;
+  return 'No qualifying billing package definition' unless @cust_pkg;
+
+  my $cust_pkg = $cust_pkg[0]; #only one
+
+  #end of false laziness
+
+  my $bill = $cust_pkg->bill || $cust_pkg->setup || time;
+
+  $cust_pkg->bill(
+    $cust_pkg->part_pkg->add_freq( $bill, $self->option('increment') )
+  );
+
+  my $error = $cust_pkg->replace;
+  die "Error incrementing next bill date: $error" if $error;
+
+  '';
+
+}
+
+1;
diff --git a/FS/FS/part_event/Action/referral_pkg_discount.pm b/FS/FS/part_event/Action/referral_pkg_discount.pm
new file mode 100644 (file)
index 0000000..2ff1b35
--- /dev/null
@@ -0,0 +1,101 @@
+package FS::part_event::Action::referral_pkg_discount;
+
+use strict;
+use base qw( FS::part_event::Action );
+
+sub description { "Discount the referring customer's package"; }
+
+#sub eventtable_hashref {
+#}
+
+sub option_fields {
+  (
+    'if_pkgpart'  => { 'label'    => 'Only packages',
+                       'type'     => 'select-part_pkg',
+                       'multiple' => 1,
+                     },
+    'discountnum' => { 'label'    => 'Discount',
+                       'type'     => 'select-table', #we don't handle the select-discount create a discount case
+                       'table'    => 'discount',
+                       'name_col' => 'description', #well, method
+                       'order_by' => 'ORDER BY discountnum', #requied because name_col is a method
+                       'hashref'  => { 'disabled' => '',
+                                       'months'   => { op=>'!=', value=>'0' },
+                                     },
+                       'disable_empty' => 1,
+                     },
+  );
+}
+
+#false laziness w/referral_pkg_billdate, probably should make
+# Mixin/referral_pkg.pm if we need changes or anything else in this vein
+sub do_action {
+  my( $self, $cust_object, $cust_event ) = @_;
+
+  my $cust_main = $self->cust_main($cust_object);
+
+  return 'No referring customer' unless $cust_main->referral_custnum;
+
+  my $referring_cust_main = $cust_main->referring_cust_main;
+  #return 'Referring customer is cancelled'
+  #  if $referring_cust_main->status eq 'cancelled';
+
+  my %if_pkgpart = map { $_=>1 } split(/\s*,\s*/, $self->option('if_pkgpart') );
+  my @cust_pkg = grep $if_pkgpart{ $_->pkgpart },
+                      $referring_cust_main->billing_pkgs;
+  return 'No qualifying billing package definition' unless @cust_pkg;
+
+  my $cust_pkg = $cust_pkg[0]; #only one
+
+  #end of false laziness
+
+  my @cust_pkg_discount = $cust_pkg->cust_pkg_discount_active;
+  my @my_cust_pkg_discount =
+    grep { $_->discountnum == $self->option('discountnum') } @cust_pkg_discount;
+
+  if ( @my_cust_pkg_discount ) { #increment the existing one instead
+
+    die "guru meditation #and: multiple discounts"
+      if scalar(@my_cust_pkg_discount) > 1;
+    my $cust_pkg_discount = $my_cust_pkg_discount[0];
+    my $discount = $cust_pkg_discount->discount;
+    die "guru meditation #goob: can't extended non-expiring discount"
+      if $discount->months == 0;
+
+    my $error = $cust_pkg_discount->decrement_months_used( $discount->months );
+    die "Error extending discount: $error\n" if $error;
+
+  } elsif ( @cust_pkg_discount ) {
+
+    #"stacked" discount case not possible from UI, not handled, so prevent
+    # against creating one here.  i guess we could try to find a different
+    # @cust_pkg above if this case needed to be handled better?
+    die "Can't discount an already discounted package";
+
+  } else { #normal case, create a new one
+
+    my $cust_pkg_discount = new FS::cust_pkg_discount {
+      'pkgnum'      => $cust_pkg->pkgnum,
+      'discountnum' => $self->option('discountnum'),
+      'months_used' => 0,
+      #'end_date'    => '',
+      #we dont handle the create a new discount case
+      #'_type'       => scalar($cgi->param('discountnum__type')),
+      #'amount'      => scalar($cgi->param('discountnum_amount')),
+      #'percent'     => scalar($cgi->param('discountnum_percent')),
+      #'months'      => scalar($cgi->param('discountnum_months')),
+      #'setup'       => scalar($cgi->param('discountnum_setup')),
+      ##'linked'       => scalar($cgi->param('discountnum_linked')),
+      ##'disabled'    => $self->discountnum_disabled,
+    };
+    my $error = $cust_pkg_discount->insert;
+    die "Error discounting package: $error\n" if $error;
+
+  }
+
+  '';
+
+}
+
+1;
diff --git a/FS/FS/part_event/Condition/cust_bill_owed_percent.pm b/FS/FS/part_event/Condition/cust_bill_owed_percent.pm
new file mode 100644 (file)
index 0000000..e06b511
--- /dev/null
@@ -0,0 +1,50 @@
+package FS::part_event::Condition::cust_bill_owed_percent;
+
+use strict;
+use FS::cust_bill;
+
+use base qw( FS::part_event::Condition );
+
+sub description {
+  'Percentage owed on specific invoice';
+}
+
+sub eventtable_hashref {
+    { 'cust_main' => 0,
+      'cust_bill' => 1,
+      'cust_pkg'  => 0,
+    };
+}
+
+sub option_fields {
+  (
+    'owed' => { 'label'      => 'Percentage of invoice owed over',
+                'type'       => 'percentage',
+                'value'      => '0', #default
+              },
+  );
+}
+
+sub condition {
+  #my($self, $cust_bill, %opt) = @_;
+  my($self, $cust_bill) = @_;
+
+  my $percent = $self->option('owed') || 0;
+  my $over = sprintf('%.2f',
+      $cust_bill->charged * $percent / 100);
+
+  $cust_bill->owed > $over;
+}
+
+sub condition_sql {
+  my( $class, $table ) = @_;
+
+  # forces the option to be an integer--do we care?
+  my $percent = $class->condition_sql_option_integer('owed');
+
+  my $owed_sql = FS::cust_bill->owed_sql;
+
+  "$owed_sql > CAST( cust_bill.charged * $percent / 100 AS DECIMAL(10,2) )";
+}
+
+1;
index c54b7e2..d85e1bd 100644 (file)
@@ -4,7 +4,7 @@ use strict;
 
 use base qw( FS::part_event::Condition );
 
-sub description { 'Customer has uncancelled package of specified definitions'; }
+sub description { 'Customer has uncancelled specific package(s)'; }
 
 sub eventtable_hashref {
     { 'cust_main' => 1,
@@ -27,7 +27,6 @@ sub condition {
 
   my $cust_main = $self->cust_main($object);
 
-  #XXX test
   my $if_pkgpart = $self->option('if_pkgpart') || {};
   grep $if_pkgpart->{ $_->pkgpart }, $cust_main->ncancelled_pkgs;
 
index dee240f..c505794 100644 (file)
@@ -13,7 +13,7 @@ sub option_fields {
                   'type'  => 'checkbox',
                   'value' => 'Y',
                 },
-    'check_bal' => { 'label' => 'Check referring custoemr balance',
+    'check_bal' => { 'label' => 'Check referring customer balance',
                      'type'  => 'checkbox',
                      'value' => 'Y',
                    },
diff --git a/FS/FS/part_event/Condition/message_email.pm b/FS/FS/part_event/Condition/message_email.pm
new file mode 100644 (file)
index 0000000..7cceba6
--- /dev/null
@@ -0,0 +1,22 @@
+package FS::part_event::Condition::message_email;
+use base qw( FS::part_event::Condition );
+use strict;
+
+sub description {
+  'Customer allows email notices'
+}
+
+sub condition {
+  my( $self, $object ) = @_;
+  my $cust_main = $self->cust_main($object);
+
+  $cust_main->message_noemail ? 0 : 1;
+}
+
+sub condition_sql {
+  my( $self, $table ) = @_;
+
+  "cust_main.message_noemail IS NULL"
+}
+
+1;
index b8a8fbf..67767f9 100644 (file)
@@ -45,7 +45,6 @@ sub condition {
 
 }
 
-#XXX test?
 sub condition_sql {
   my( $self, $table ) = @_;
 
index f85a056..1ee53b8 100644 (file)
@@ -12,6 +12,15 @@ sub description { "Run only once for each time the package has been billed"; }
 # Run the event, at most, a number of times equal to the number of 
 # distinct invoices that contain line items from this package.
 
+sub option_fields {
+  (
+    'paid' => { 'label' => 'Only count paid bills',
+                'type'  => 'checkbox',
+                'value' => 'Y',
+              },
+  )
+}
+
 sub eventtable_hashref {
     { 'cust_main' => 0,
       'cust_bill' => 0,
@@ -22,9 +31,15 @@ sub eventtable_hashref {
 sub condition {
   my($self, $cust_pkg, %opt) = @_;
 
-  my %invnum;
-  $invnum{$_->invnum} = 1 
-    foreach ( qsearch('cust_bill_pkg', { 'pkgnum' => $cust_pkg->pkgnum }) );
+  my @cust_bill_pkg = qsearch('cust_bill_pkg', { pkgnum=>$cust_pkg->pkgnum });
+
+  @cust_bill_pkg = grep { ($_->owed_setup + $_->owed_recur) == 0 }
+                     @cust_bill_pkg
+    if $self->option('paid');
+
+  my %invnum = ();
+  $invnum{$_->invnum} = 1 foreach @cust_bill_pkg;
+
   my @events = qsearch( {
       'table'     => 'cust_event', 
       'hashref'   => { 'eventpart' => $self->eventpart,
@@ -40,6 +55,9 @@ sub condition {
 sub condition_sql {
   my( $self, $table ) = @_;
 
+  #paid flag not yet implemented here, but that's okay, a partial optimization
+  # is better than none
+
   "( 
     ( SELECT COUNT(distinct(invnum)) 
       FROM cust_bill_pkg
diff --git a/FS/FS/part_event/Condition/times_percust.pm b/FS/FS/part_event/Condition/times_percust.pm
new file mode 100644 (file)
index 0000000..fc7064b
--- /dev/null
@@ -0,0 +1,76 @@
+package FS::part_event::Condition::times_percust;
+
+use strict;
+use FS::Record qw( qsearch );
+use FS::part_event;
+use FS::cust_event;
+
+use base qw( FS::part_event::Condition );
+
+sub description { "Run this event the specified number of times per customer"; }
+
+sub option_fields {
+  (
+    'run_times'  => { label=>'Number of times', type=>'text', value=>'1', },
+  );
+}
+
+sub eventtable_hashref {
+    { 'cust_main' => 0,
+      'cust_bill' => 1,
+      'cust_pkg'  => 1,
+    };
+}
+
+sub condition {
+  my($self, $object, %opt) = @_;
+
+  my $obj_pkey = $object->primary_key;
+  my $obj_table = $object->table;
+  my $custnum = $object->custnum;
+
+  my @where = (
+    "tablenum IN ( SELECT $obj_pkey FROM $obj_table WHERE custnum = $custnum )"
+  );
+  if ( $opt{'cust_event'}->eventnum =~ /^(\d+)$/ ) {
+    push @where, " eventnum != $1 ";
+  }
+  my $extra_sql = ' AND '. join(' AND ', @where);
+  my @existing = qsearch( {
+    'table'     => 'cust_event',
+    'hashref'   => {
+                     'eventpart' => $self->eventpart,
+                     #'tablenum'  => $tablenum,
+                     'status'    => { op=>'!=', value=>'failed' },
+                   },
+    'extra_sql' => $extra_sql,
+  } );
+
+  scalar(@existing) < $self->option('run_times');
+
+}
+
+sub condition_sql {
+  my( $class, $table, %opt ) = @_;
+
+  my %pkey = %{ FS::part_event->eventtable_pkey };
+
+  my $run_times =
+    $class->condition_sql_option_integer('run_times', $opt{'driver_name'});
+
+  my $pkey = $pkey{$table};
+
+  my $existing = "( SELECT COUNT(*) FROM cust_event
+                      WHERE cust_event.eventpart = part_event.eventpart
+                        AND cust_event.tablenum IN (
+                          SELECT $pkey FROM $table AS times_percust
+                            WHERE times_percust.custnum = cust_main.custnum )
+                        AND status != 'failed'
+                  )";
+
+  "$existing < $run_times";
+
+}
+
+1;
index 5d65062..28cb141 100644 (file)
@@ -125,31 +125,14 @@ sub insert {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  my $error = $self->SUPER::insert(@_);
+  my $error = $self->SUPER::insert(@_)
+           || $self->replace;
+  # use replace to do all the part_export_machine and default_machine stuff
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   }
 
-  #kinda false laziness with process_m2name
-  my @machines = map { $_ =~ s/^\s+//; $_ =~ s/\s+$//; $_ }
-                   grep /\S/,
-                     split /[\n\r]{1,2}/,
-                       $self->part_export_machine_textarea;
-
-  foreach my $machine ( @machines ) {
-
-    my $part_export_machine = new FS::part_export_machine {
-      'exportnum' => $self->exportnum,
-      'machine'   => $machine,
-    };
-    $error = $part_export_machine->insert;
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return $error;
-    }
-  }
-
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
 }
@@ -217,6 +200,7 @@ or modified.
 
 sub replace {
   my $self = shift;
+  my $old = $self->replace_old;
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
@@ -228,12 +212,7 @@ sub replace {
   my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
-
-  my $error = $self->SUPER::replace(@_);
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return $error;
-  }
+  my $error;
 
   if ( $self->part_export_machine_textarea ) {
 
@@ -258,6 +237,10 @@ sub replace {
           }
         }
 
+        if ( $self->default_machine_name eq $machine ) {
+          $self->default_machine( $part_export_machine{$machine}->machinenum );
+        }
+
         delete $part_export_machine{$machine}; #so we don't disable it below
 
       } else {
@@ -272,11 +255,13 @@ sub replace {
           return $error;
         }
   
+        if ( $self->default_machine_name eq $machine ) {
+          $self->default_machine( $part_export_machine->machinenum );
+        }
       }
 
     }
 
-
     foreach my $part_export_machine ( values %part_export_machine ) {
       $part_export_machine->disabled('Y');
       $error = $part_export_machine->replace;
@@ -286,6 +271,48 @@ sub replace {
       }
     }
 
+    if ( $old->machine ne '_SVC_MACHINE' ) {
+      # then set up the default for any already-attached export_svcs
+      foreach my $export_svc ( $self->export_svc ) {
+        my @svcs = qsearch('cust_svc', { 'svcpart' => $export_svc->svcpart });
+        foreach my $cust_svc ( @svcs ) {
+          my $svc_export_machine = FS::svc_export_machine->new({
+              'exportnum'   => $self->exportnum,
+              'svcnum'      => $cust_svc->svcnum,
+              'machinenum'  => $self->default_machine,
+          });
+          $error ||= $svc_export_machine->insert;
+        }
+      }
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    } # if switching to selectable hosts
+
+  } elsif ( $old->machine eq '_SVC_MACHINE' ) {
+    # then we're switching from selectable to non-selectable
+    foreach my $svc_export_machine (
+      qsearch('svc_export_machine', { 'exportnum' => $self->exportnum })
+    ) {
+      $error ||= $svc_export_machine->delete;
+    }
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+
+  }
+
+  $error = $self->SUPER::replace(@_);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( $self->machine eq '_SVC_MACHINE' and ! $self->default_machine ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "no default export host selected";
   }
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
@@ -308,6 +335,13 @@ sub check {
     || $self->ut_domainn('machine')
     || $self->ut_alpha('exporttype')
   ;
+
+  if ( $self->machine eq '_SVC_MACHINE' ) {
+    $error ||= $self->ut_numbern('default_machine')
+  } else {
+    $self->set('default_machine', '');
+  }
+
   return $error if $error;
 
   $self->nodomain =~ /^(Y?)$/ or return "Illegal nodomain: ". $self->nodomain;
@@ -471,7 +505,9 @@ sub _rebless {
   $self;
 }
 
-=item svc_machine
+=item svc_machine SVC_X
+
+Return the export hostname for SVC_X.
 
 =cut
 
@@ -483,14 +519,33 @@ sub svc_machine {
   my $svc_export_machine = qsearchs('svc_export_machine', {
     'svcnum'    => $svc_x->svcnum,
     'exportnum' => $self->exportnum,
-  })
-    #would only happen if you add this export to existing services without a
-    #machine set then try to run exports without setting it... right?
-    or die "No hostname selected for ".($self->exportname || $self->exporttype);
+  });
+
+  if (!$svc_export_machine) {
+    warn "No hostname selected for ".($self->exportname || $self->exporttype);
+    return $self->default_export_machine->machine;
+  }
 
   return $svc_export_machine->part_export_machine->machine;
 }
 
+=item default_export_machine
+
+Return the default export hostname for this export.
+
+=cut
+
+sub default_export_machine {
+  my $self = shift;
+  my $machinenum = $self->default_machine;
+  if ( $machinenum ) {
+    my $default_machine = FS::part_export_machine->by_key($machinenum);
+    return $default_machine->machine if $default_machine;
+  }
+  # this should not happen
+  die "no default export hostname for export ".$self->exportnum;
+}
+
 #these should probably all go away, just let the subclasses define em
 
 =item export_insert SVC_OBJECT
@@ -601,6 +656,17 @@ DEFAULTSREF is a hashref with the same keys where true values indicate the
 setting is a default (and thus can be displayed in the UI with less emphasis,
 or hidden by default).
 
+=item actions
+
+Adds one or more "action" links to the export's display in 
+browse/part_export.cgi.  Should return pairs of values.  The first is 
+the link label; the second is the Mason path to a document to load.
+The document will show in a popup.
+
+=cut
+
+sub actions { }
+
 =cut
 
 =item weight
@@ -630,6 +696,10 @@ sub info {
 
 #default fallbacks... FS::part_export::DID_Common ?
 sub get_dids_can_tollfree { 0; }
+sub get_dids_can_manual   { 0; }
+sub get_dids_can_edit     { 0; } #don't use without can_manual, otherwise the
+                                 # DID selector provisions a new number from
+                                 # inventory each edit
 sub get_dids_npa_select   { 1; }
 
 =back
@@ -688,6 +758,55 @@ sub _upgrade_data {  #class method
     $error = $opt->replace;
     die $error if $error;
   }
+  # for exports that have selectable hostnames, make sure all services
+  # have a hostname selected
+  foreach my $part_export (
+    qsearch('part_export', { 'machine' => '_SVC_MACHINE' })
+  ) {
+
+    my $exportnum = $part_export->exportnum;
+    my $machinenum = $part_export->default_machine;
+    if (!$machinenum) {
+      my ($first) = $part_export->part_export_machine;
+      if (!$first) {
+        # user intervention really is required.
+        die "Export $exportnum has no hostname options defined.\n".
+            "You must correct this before upgrading.\n";
+      }
+      # warn about this, because we might not choose the right one
+      warn "Export $exportnum (". $part_export->exporttype.
+           ") has no default hostname.  Setting to ".$first->machine."\n";
+      $machinenum = $first->machinenum;
+      $part_export->set('default_machine', $machinenum);
+      my $error = $part_export->replace;
+      die $error if $error;
+    }
+
+    # the service belongs to a service def that uses this export
+    # and there is not a hostname selected for this export for that service
+    my $join = ' JOIN export_svc USING ( svcpart )'.
+               ' LEFT JOIN svc_export_machine'.
+               ' ON ( cust_svc.svcnum = svc_export_machine.svcnum'.
+               ' AND export_svc.exportnum = svc_export_machine.exportnum )';
+
+    my @svcs = qsearch( {
+          'select'    => 'cust_svc.*',
+          'table'     => 'cust_svc',
+          'addl_from' => $join,
+          'extra_sql' => ' WHERE svcexportmachinenum IS NULL'.
+                         ' AND export_svc.exportnum = '.$part_export->exportnum,
+      } );
+    foreach my $cust_svc (@svcs) {
+      my $svc_export_machine = FS::svc_export_machine->new({
+          'exportnum'   => $exportnum,
+          'machinenum'  => $machinenum,
+          'svcnum'      => $cust_svc->svcnum,
+      });
+      my $error = $svc_export_machine->insert;
+      die $error if $error;
+    }
+  }
+
   # pass downstream
   my %exports_in_use;
   $exports_in_use{ref $_} = 1 foreach qsearch('part_export', {});
index a493f52..acd7ffe 100644 (file)
@@ -131,10 +131,10 @@ sub _export_command {
 sub _export_replace {
   my( $self, $new, $old ) = (shift, shift, shift);
 
-  my $method = $self->option($action.'_method');
+  my $method = $self->option('replace_method');
   return '' if $method =~ /^\s*$/;
 
-  my @params = split("\n", $self->option($action.'_params') );
+  my @params = split("\n", $self->option('replace_params') );
 
   my( @x_param ) = ();
   my( %x_struct ) = ();
diff --git a/FS/FS/part_export/dma_radiusmanager.pm b/FS/FS/part_export/dma_radiusmanager.pm
deleted file mode 100644 (file)
index d46a996..0000000
+++ /dev/null
@@ -1,355 +0,0 @@
-package FS::part_export::dma_radiusmanager;
-
-use strict;
-use vars qw($DEBUG %info %options);
-use base 'FS::part_export';
-use FS::part_svc;
-use FS::svc_acct;
-use FS::radius_group;
-use Tie::IxHash;
-use Digest::MD5 'md5_hex';
-
-use Locale::Country qw(code2country);
-use Locale::SubCountry;
-use Date::Format 'time2str';
-
-tie %options, 'Tie::IxHash',
-  'dbname'    => { label=>'Database name', default=>'radius' },
-  'username'  => { label=>'Database username' },
-  'password'  => { label=>'Database password' },
-  'manager'   => { label=>'Manager name' },
-  'template_name'   => { label=>'Template service name' },
-  'service_prefix'  => { label=>'Service name prefix' },
-  'debug'     => { label=>'Enable debugging', type=>'checkbox' },
-;
-
-%info = (
-  'svc'       => 'svc_acct',
-  'desc'      => 'Export to DMA Radius Manager',
-  'options'   => \%options,
-  'nodomain'  => 'Y',
-  'notes'     => '', #XXX
-);
-
-$DEBUG = 0;
-
-sub connect {
-  my $self = shift;
-  my $datasrc = 'dbi:mysql:host='.$self->machine.
-                ':database='.$self->option('dbname');
-  DBI->connect(
-    $datasrc,
-    $self->option('username'),
-    $self->option('password'),
-    { AutoCommit => 0 }
-  ) or die $DBI::errstr;
-}
-
-sub export_insert  { my $self = shift; $self->dma_rm_queue('insert', @_) }
-sub export_delete  { my $self = shift; $self->dma_rm_queue('delete', @_) }
-sub export_replace { my $self = shift; $self->dma_rm_queue('replace', @_) }
-sub export_suspend { my $self = shift; $self->dma_rm_queue('suspend', @_) }
-sub export_unsuspend { my $self = shift; $self->dma_rm_queue('unsuspend', @_) }
-
-sub dma_rm_queue {
-  my ($self, $action, $svc_acct, $old) = @_;
-
-  my $svcnum = $svc_acct->svcnum;
-
-  my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
-  my $cust_main = $cust_pkg->cust_main;
-  my $location = $cust_pkg->cust_location;
-
-  my $address = $location->address1;
-  $address .= ' '.$location->address2 if $location->address2;
-  my $country = code2country($location->country);
-  my $lsc = Locale::SubCountry->new($location->country);
-  my $state = $lsc->full_name($location->state) if defined($lsc);
-
-  my %params = (
-    # for the remote side
-    username    => $svc_acct->username,
-    password    => md5_hex($svc_acct->_password),
-    groupid     => $self->option('groupid'),
-    enableuser  => 1,
-    firstname   => $cust_main->first,
-    lastname    => $cust_main->last,
-    company     => $cust_main->company,
-    phone       => ($cust_main->daytime || $cust_main->night),
-    mobile      => $cust_main->mobile,
-    address     => $location->address1, # address2?
-    city        => $location->city,
-    state       => $state, #full name
-    zip         => $location->zip,
-    country     => $country, #full name
-    gpslat      => $location->latitude,
-    gpslong     => $location->longitude,
-    comment     => 'svcnum'.$svcnum,
-    createdby   => $self->option('manager'),
-    owner       => $self->option('manager'),
-    email       => $cust_main->invoicing_list_emailonly_scalar,
-
-    # used internally by the export
-    exportnum   => $self->exportnum,
-    svcnum      => $svcnum,
-    action      => $action,
-    svcpart     => $svc_acct->cust_svc->svcpart,
-    _password   => $svc_acct->_password,
-  );
-  if ( $action eq 'replace' ) {
-    $params{'old_username'} = $old->username;
-    $params{'old_password'} = $old->_password;
-  }
-  my $queue = FS::queue->new({
-      'svcnum'  => $svcnum,
-      'job'     => "FS::part_export::dma_radiusmanager::dma_rm_action",
-  });
-  $queue->insert(%params);
-}
-
-sub dma_rm_action {
-  my %params = @_;
-  my $svcnum = delete $params{svcnum};
-  my $action = delete $params{action};
-  my $svcpart = delete $params{svcpart};
-  my $exportnum = delete $params{exportnum};
-
-  my $username = $params{username};
-  my $password = delete $params{_password};
-
-  my $self = FS::part_export->by_key($exportnum);
-  my $dbh = $self->connect;
-  local $DEBUG = 1 if $self->option('debug');
-
-  # export the part_svc if needed, and get its srvid
-  my $part_svc = FS::part_svc->by_key($svcpart);
-  my $srvid = $self->export_part_svc($part_svc, $dbh); # dies on error
-  $params{srvid} = $srvid;
-
-  if ( $action eq 'insert' ) {
-    $params{'createdon'} = time2str('%Y-%m-%d', time);
-    $params{'expiration'} = time2str('%Y-%m-%d', time);
-    warn "rm_users: inserting svcnum$svcnum\n" if $DEBUG;
-    my $sth = $dbh->prepare( 'INSERT INTO rm_users ( '.
-      join(', ', keys(%params)).
-      ') VALUES ('.
-      join(', ', ('?') x keys(%params)).
-      ')'
-    );
-    $sth->execute(values(%params)) or die $dbh->errstr;
-
-    # minor false laziness w/ sqlradius_insert
-    warn "radcheck: inserting $username\n" if $DEBUG;
-    $sth = $dbh->prepare( 'INSERT INTO radcheck (
-      username, attribute, op, value
-    ) VALUES (?, ?, ?, ?)' );
-    $sth->execute(
-      $username,
-      'Cleartext-Password',
-      ':=', # :=(
-      $password,
-    ) or die $dbh->errstr;
-
-    $sth->execute(
-      $username,
-      'Simultaneous-Use',
-      ':=',
-      1, # should this be an option?
-    ) or die $dbh->errstr;
-    # also, we don't support exporting any other radius attrs...
-    # those should go in 'custattr' if we need them
-  } elsif ( $action eq 'replace' ) {
-
-    my $old_username = delete $params{old_username};
-    my $old_password = delete $params{old_password};
-    # svcnum is invariant and on the remote side, so we don't need any 
-    # of the old fields to do this
-    warn "rm_users: updating svcnum$svcnum\n" if $DEBUG;
-    my $sth = $dbh->prepare( 'UPDATE rm_users SET '.
-      join(', ', map { "$_ = ?" } keys(%params)).
-      ' WHERE comment = ?'
-    );
-    $sth->execute(values(%params), $params{comment}) or die $dbh->errstr;
-    # except for username/password changes
-    if ( $old_password ne $password ) {
-      warn "radcheck: changing password for $old_username\n" if $DEBUG;
-      $sth = $dbh->prepare( 'UPDATE radcheck SET value = ? '.
-        'WHERE username = ? and attribute = \'Cleartext-Password\''
-      );
-      $sth->execute($password, $old_username) or die $dbh->errstr;
-    }
-    if ( $old_username ne $username ) {
-      warn "radcheck: changing username $old_username to $username\n"
-        if $DEBUG;
-      $sth = $dbh->prepare( 'UPDATE radcheck SET username = ? '.
-        'WHERE username = ?'
-      );
-      $sth->execute($username, $old_username) or die $dbh->errstr;
-    }
-
-  } elsif ( $action eq 'suspend' ) {
-
-    # this is sufficient
-    warn "rm_users: disabling svcnum#$svcnum\n" if $DEBUG;
-    my $sth = $dbh->prepare( 'UPDATE rm_users SET enableuser = 0 '.
-      'WHERE comment = ?'
-    );
-    $sth->execute($params{comment}) or die $dbh->errstr;
-
-  } elsif ( $action eq 'unsuspend' ) {
-
-    warn "rm_users: enabling svcnum#$svcnum\n" if $DEBUG;
-    my $sth = $dbh->prepare( 'UPDATE rm_users SET enableuser = 1 '.
-      'WHERE comment = ?'
-    );
-    $sth->execute($params{comment}) or die $dbh->errstr;
-
-  } elsif ( $action eq 'delete' ) {
-
-    warn "rm_users: deleting svcnum#$svcnum\n" if $DEBUG;
-    my $sth = $dbh->prepare( 'DELETE FROM rm_users WHERE comment = ?' );
-    $sth->execute($params{comment}) or die $dbh->errstr;
-
-    warn "radcheck: deleting $username\n" if $DEBUG;
-    $sth = $dbh->prepare( 'DELETE FROM radcheck WHERE username = ?' );
-    $sth->execute($username) or die $dbh->errstr;
-
-    # if this were smarter it would also delete the rm_services record
-    # if it was no longer in use, but that's not really necessary
-  }
-
-  $dbh->commit;
-  '';
-}
-
-=item export_part_svc PART_SVC DBH
-
-Query Radius Manager for a service definition matching the name of 
-PART_SVC (optionally with a prefix defined in the export options).  
-If there is one, update it to match the attributes of PART_SVC; if 
-not, create one.  Then return its srvid.
-
-=cut
-
-sub export_part_svc {
-  my ($self, $part_svc, $dbh) = @_;
-
-  # if $dbh exists, use the existing transaction
-  # otherwise create our own and commit when finished
-  my $commit = 0;
-  if (!$dbh) {
-    $dbh = $self->connect;
-    $commit = 1;
-  }
-
-  my $name = $self->option('service_prefix').$part_svc->svc;
-
-  my %params = (
-    'srvname'         => $name,
-    'enableservice'   => 1,
-    'nextsrvid'       => -1,
-    'dailynextsrvid'  => -1,
-    # force price-related fields to zero
-    'unitprice'       => 0,
-    'unitpriceadd'    => 0,
-    'unitpricetax'    => 0,
-    'unitpriceaddtax' => 0,
-  );
-  my @fixed_groups;
-  # use speed settings from fixed usergroups configured on this part_svc
-  if ( my $psc = $part_svc->part_svc_column('usergroup') ) {
-    # each part_svc really should only have one fixed group with non-null 
-    # speed settings, but go by priority order for consistency
-    @fixed_groups = 
-      sort { $a->priority <=> $b->priority }
-      grep { $_ }
-      map { FS::radius_group->by_key($_) }
-      split(/\s*,\s*/, $psc->columnvalue);
-  } # otherwise there are no fixed groups, so leave speed empty
-
-  foreach (qw(down up)) {
-    my $speed = "speed_$_";
-    foreach my $group (@fixed_groups) {
-      if ( ($group->$speed || 0) > 0 ) {
-        $params{$_.'rate'} = $group->$speed;
-        last;
-      }
-    }
-  }
-  # anything else we need here? poolname, maybe?
-
-  warn "rm_services: looking for '$name'\n" if $DEBUG;
-  my $sth = $dbh->prepare( 
-    'SELECT srvid FROM rm_services WHERE srvname = ? AND enableservice = 1'
-  );
-  $sth->execute($name) or die $dbh->errstr;
-  if ( $sth->rows > 1 ) {
-    die "Multiple services with name '$name' found in Radius Manager.\n";
-
-  } elsif ( $sth->rows == 0 ) {
-    # leave this blank to disable creating new service defs
-    my $template_name = $self->option('template_name');
-
-    die "Can't create a new service profile--no template service specified.\n"
-      unless $template_name;
-
-    warn "rm_services: fetching template '$template_name'\n" if $DEBUG;
-    $sth = $dbh->prepare('SELECT * FROM rm_services WHERE srvname = ? LIMIT 1');
-    $sth->execute($template_name);
-    die "Can't create a new service profile--template service ".
-      "'$template_name' not found.\n" unless $sth->rows == 1;
-    my $template = $sth->fetchrow_hashref;
-    %params = (%$template, %params);
-
-    # get the next available srvid
-    $sth = $dbh->prepare('SELECT MAX(srvid) FROM rm_services');
-    $sth->execute or die $dbh->errstr;
-    my $srvid;
-    if ( $sth->rows ) {
-      $srvid = $sth->fetchrow_arrayref->[0] + 1;
-    }
-    $params{'srvid'} = $srvid;
-
-    # create a new one based on the template
-    warn "rm_services: inserting '$name' as srvid#$srvid\n" if $DEBUG;
-    $sth = $dbh->prepare(
-      'INSERT INTO rm_services ('.join(', ', keys %params).
-      ') VALUES ('.join(', ', map {'?'} keys %params).')'
-    );
-    $sth->execute(values(%params)) or die $dbh->errstr;
-    # also link it to all the managers allowed on the template service
-    warn "rm_services: linking to manager\n" if $DEBUG;
-    $sth = $dbh->prepare(
-      'INSERT INTO rm_allowedmanagers (srvid, managername) '.
-      'SELECT ?, managername FROM rm_allowedmanagers WHERE srvid = ?'
-    );
-    $sth->execute($srvid, $template->{srvid}) or die $dbh->errstr;
-    # and the same for NASes
-    warn "rm_services: linking to nas\n" if $DEBUG;
-    $sth = $dbh->prepare(
-      'INSERT INTO rm_allowednases (srvid, nasid) '.
-      'SELECT ?, nasid FROM rm_allowednases WHERE srvid = ?'
-    );
-    $sth->execute($srvid, $template->{srvid}) or die $dbh->errstr;
-
-    $dbh->commit if $commit;
-    return $srvid;
-
-  } else { # $sth->rows == 1, it already exists
-
-    my $row = $sth->fetchrow_arrayref;
-    my $srvid = $row->[0];
-    warn "rm_services: updating srvid#$srvid\n" if $DEBUG;
-    $sth = $dbh->prepare(
-      'UPDATE rm_services SET '.join(', ', map {"$_ = ?"} keys %params) .
-      ' WHERE srvid = ?'
-    );
-    $sth->execute(values(%params), $srvid) or die $dbh->errstr;
-
-    $dbh->commit if $commit;
-    return $srvid;
-
-  }
-}
-
-1;
index fb03785..a51457a 100644 (file)
@@ -28,6 +28,8 @@ tie my %options, 'Tie::IxHash',
 sub rebless { shift; }
 
 sub get_dids_can_tollfree { 0; };
+sub get_dids_can_manual   { 1; };
+sub get_dids_can_edit     { 1; };
 sub get_dids_npa_select   { 0; };
 
 # i guess we could get em from the API, but since its returning states without
index 6fbd3fb..80139e7 100644 (file)
@@ -3,28 +3,53 @@ use base qw( FS::part_export );
 
 use strict;
 use warnings;
-use vars qw( %info );
+use vars qw( %info $DEBUG );
+use URI::Escape;
 use LWP::UserAgent;
 use HTTP::Request::Common;
+use Email::Valid;
 
 tie my %options, 'Tie::IxHash',
   'url' => { label => 'URL', },
+  'blacklist_add_url' => { label => 'Optional blacklist add URL', },
+  'blacklist_del_url' => { label => 'Optional blacklist delete URL', },
+  'whitelist_add_url' => { label => 'Optional whitelist add URL', },
+  'whitelist_del_url' => { label => 'Optional whitelist delete URL', },
+  'vacation_add_url'  => { label => 'Optional vacation message add URL', },
+  'vacation_del_url'  => { label => 'Optional vacation message delete URL', },
+
   #'user'     => { label => 'Username', default=>'' },
   #'password' => { label => 'Password', default => '' },
 ;
 
 %info = (
-  'svc'     => 'svc_dsl',
+  'svc'     => [ 'svc_acct', 'svc_dsl', ],
   'desc'    => 'Retrieve status information via HTTP or HTTPS',
   'options' => \%options,
   'no_machine' => 1,
   'notes'   => <<'END'
 Fields from the service can be substituted in the URL as $field.
+
+Optionally, spam black/whitelist addresees and a vacation message may be
+modified via HTTP or HTTPS as well.
 END
 );
 
+$DEBUG = 1;
+
 sub rebless { shift; }
 
+our %addl_fields = (
+  'svc_acct' => [qw( email ) ],
+  'svc_dsl'  => [qw( gateway_access_or_phonenum ) ],
+);
+
+#some NOPs for required subroutines, to avoid throwing the exceptions in the
+# part_export.pm fallbacks
+sub _export_insert  { '' };
+sub _export_replace { '' };
+sub _export_delete  { '' };
+
 sub export_getstatus {
   my( $self, $svc_x, $htmlref, $hashref ) = @_;
 
@@ -34,10 +59,97 @@ sub export_getstatus {
   {
     no strict 'refs';
     ${$_} = $svc_x->getfield($_) foreach $svc_x->fields;
-    if ( $svc_x->table eq 'svc_dsl' ) {
-      ${$_} = $svc_x->$_() foreach (qw( gateway_access_or_phonenum ));
+    ${$_} = $svc_x->$_()         foreach @{ $addl_fields{ $svc_x->table } };
+    $url = eval(qq("$urlopt"));
+  }
+
+  my $req = HTTP::Request::Common::GET( $url );
+  my $ua = LWP::UserAgent->new;
+  my $response = $ua->request($req);
+
+  if ( $svc_x->table eq 'svc_dsl' ) {
+
+    $$htmlref = $response->is_error ? $response->error_as_HTML
+                                    : $response->content;
+
+    #hash data not yet implemented for svc_dsl
+
+  } elsif ( $svc_x->table eq 'svc_acct' ) {
+
+    #this whole section is rather specific to fibernetics and should be an
+    # option or callback or something
+
+    # to,from,wb_value
+
+    use Text::CSV_XS;
+    my $csv = Text::CSV_XS->new;
+
+    my @lines = split("\n", $response->content);
+    pop @lines if $lines[-1] eq '';
+    my $header = shift @lines;
+    $csv->parse($header) or return;
+    my @header = $csv->fields;
+
+    while ( my $line = shift @lines ) {
+      $csv->parse($line) or next;
+      my @fields = $csv->fields;
+      my %hash = map { $_ => shift(@fields) } @header;
+
+      if ( defined $hash{'wb_value'} ) {
+        if ( $hash{'wb_value'} =~ /^[WA]/i ) { #Whitelist/Allow
+          push @{ $hashref->{'whitelist'} }, $hash{'from'};
+        } else { # if ( $hash{'wb_value'} =~ /^[BD]/i ) { #Blacklist/Deny
+          push @{ $hashref->{'blacklist'} }, $hash{'from'};
+        }
+      }
+
+      for (qw( created enddate )) {
+        $hash{$_} = '' if $hash{$_} =~ /^0000-/;
+        $hash{$_} = (split(' ', $hash{$_}))[0];
+      }
+
+      next unless $hash{'active'};
+      $hashref->{"vacation_$_"} = $hash{$_} || ''
+        foreach qw( active subject body created enddate );
+
     }
 
+  } #else { die 'guru meditation #295'; }
+
+}
+
+sub export_setstatus_listadd {
+  my( $self, $svc_x, $hr ) = @_;
+  $self->export_setstatus_listX( $svc_x, 'add', $hr->{list}, $hr->{address} );
+}
+
+sub export_setstatus_listdel {
+  my( $self, $svc_x, $hr ) = @_;
+  $self->export_setstatus_listX( $svc_x, 'del', $hr->{list}, $hr->{address} );
+}
+
+sub export_setstatus_listX {
+  my( $self, $svc_x, $action, $list, $address ) = @_;
+
+  my $option;
+  if ( $list =~ /^[WA]/i ) { #Whitelist/Allow
+    $option = 'whitelist_';
+  } else { # if ( $hash{'wb_value'} =~ /^[BD]/i ) { #Blacklist/Deny
+    $option = 'blacklist_';
+  }
+  $option .= $action. '_url';
+
+  $address = Email::Valid->address($address)
+    or die "address failed $Email::Valid::Details check.\n";
+
+  #some false laziness w/export_getstatus above
+  my $url;
+  my $urlopt = $self->option($option) or return; #DIFF
+  no strict 'vars';
+  {
+    no strict 'refs';
+    ${$_} = $svc_x->getfield($_) foreach $svc_x->fields;
+    ${$_} = $svc_x->$_()         foreach @{ $addl_fields{ $svc_x->table } };
     $url = eval(qq("$urlopt"));
   }
 
@@ -45,11 +157,56 @@ sub export_getstatus {
   my $ua = LWP::UserAgent->new;
   my $response = $ua->request($req);
 
-  $$htmlref = $response->is_error ? $response->error_as_HTML
-                                  : $response->content;
+  die $response->code. ' '. $response->message if $response->is_error;
 
-  #hash data note yet implemented for this status export
+}
 
+sub export_setstatus_vacationadd {
+  my( $self, $svc_x, $hr ) = @_;
+  $self->export_setstatus_vacationX( $svc_x, 'add', $hr );
 }
 
+sub export_setstatus_vacationdel {
+  my( $self, $svc_x, $hr ) = @_;
+  $self->export_setstatus_vacationX( $svc_x, 'del', $hr );
+}
+
+sub export_setstatus_vacationX {
+  my( $self, $svc_x, $action, $hr ) = @_;
+
+  my $option = 'vacation_'. $action. '_url';
+
+  my $subject = uri_escape($hr->{subject});
+  my $body    = uri_escape($hr->{body});
+  for (qw( created enddate )) {
+    if ( $hr->{$_} =~ /^(\d{4}-\d{2}-\d{2})$/ ) {
+      $hr->{$_} = $1;
+    } else {
+      $hr->{$_} = '';
+    }
+  }
+  my $created = $hr->{created};
+  my $enddate = $hr->{enddate};
+
+  #some false laziness w/export_getstatus above
+  my $url;
+  my $urlopt = $self->option($option) or return; #DIFF
+  no strict 'vars';
+  {
+    no strict 'refs';
+    ${$_} = $svc_x->getfield($_) foreach $svc_x->fields;
+    ${$_} = $svc_x->$_()         foreach @{ $addl_fields{ $svc_x->table } };
+    $url = eval(qq("$urlopt"));
+  }
+
+  my $req = HTTP::Request::Common::GET( $url );
+  my $ua = LWP::UserAgent->new;
+  my $response = $ua->request($req);
+
+  die $response->code. ' '. $response->message if $response->is_error;
+
+}
+
+1;
+
 1;
diff --git a/FS/FS/part_export/huawei_hlr.pm b/FS/FS/part_export/huawei_hlr.pm
new file mode 100644 (file)
index 0000000..0079818
--- /dev/null
@@ -0,0 +1,340 @@
+package FS::part_export::huawei_hlr;
+
+use vars qw(@ISA %info $DEBUG $CACHE);
+use Tie::IxHash;
+use FS::Record qw(qsearch qsearchs dbh);
+use FS::part_export;
+use FS::svc_phone;
+use FS::inventory_class;
+use FS::inventory_item;
+use IO::Socket::INET;
+use Data::Dumper;
+use MIME::Base64 qw(decode_base64);
+use Storable qw(thaw);
+
+use strict;
+
+$DEBUG = 0;
+@ISA = qw(FS::part_export);
+
+tie my %options, 'Tie::IxHash',
+  'opname'    => { label=>'Operator login' },
+  'pwd'       => { label=>'Operator password' },
+  'tplid'     => { label=>'Template number' },
+  'hlrsn'     => { label=>'HLR serial number' },
+  'k4sno'     => { label=>'K4 serial number' },
+  'cardtype'  => { label  => 'Card type',
+                   type   => 'select', 
+                   options=> ['SIM', 'USIM']
+                 },
+  'alg'       => { label  => 'Authentication algorithm',
+                   type   => 'select',
+                   options=> ['COMP128_1',
+                              'COMP128_2',
+                              'COMP128_3',
+                              'MILENAGE' ],
+                 },
+  'opcvalue'  => { label=>'OPC value (for MILENAGE only)' },
+  'opsno'     => { label=>'OP serial number (for MILENAGE only)' },
+  'timeout'   => { label=>'Timeout (seconds)', default => 120 },
+  'debug'     => { label=>'Enable debugging', type=>'checkbox' },
+;
+
+%info = (
+  'svc'     => 'svc_phone',
+  'desc'    => 'Provision mobile phone service to Huawei HLR9820',
+  'options' => \%options,
+  'notes'   => <<'END'
+Connects to a Huawei Subscriber Management Unit via TCP and configures mobile
+phone services according to a template.  The <i>sim_imsi</i> field must be 
+set on the service, and the template must exist.
+END
+);
+
+sub actions {
+  'Import SIMs' => 'misc/part_export/huawei_hlr-import_sim.html'
+}
+
+sub _export_insert {
+  my( $self, $svc_phone ) = (shift, shift);
+  # svc_phone::check should ensure phonenum and sim_imsi are numeric
+  my @command = (
+    IMSI   => '"'.$svc_phone->sim_imsi.'"',
+    ISDN   => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"',
+    TPLID  => $self->option('tplid'),
+  );
+  unshift @command, 'HLRSN', $self->option('hlrsn')
+    if $self->option('hlrsn');
+  unshift @command, 'ADD TPLSUB';
+  my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command);
+  ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+sub _export_replace  {
+  my( $self, $new, $old ) = @_;
+  my $depend_jobnum;
+  if ( $new->sim_imsi ne $old->sim_imsi ) {
+    my @command = (
+      'MOD IMSI',
+      ISDN    => '"'.$old->countrycode.$old->phonenum.'"',
+      IMSI    => '"'.$old->sim_imsi.'"',
+      NEWIMSI => '"'.$new->sim_imsi.'"',
+    );
+    my $err_or_queue = $self->queue_command($new->svcnum, @command);
+    return $err_or_queue unless ref $err_or_queue;
+    $depend_jobnum = $err_or_queue->jobnum;
+  }
+  if ( $new->countrycode ne $old->countrycode or 
+       $new->phonenum ne $old->phonenum ) {
+    my @command = (
+      'MOD ISDN',
+      ISDN    => '"'.$old->countrycode.$old->phonenum.'"',
+      NEWISDN => '"'.$new->countrycode.$new->phonenum.'"',
+    );
+    my $err_or_queue = $self->queue_command($new->svcnum, @command);
+    return $err_or_queue unless ref $err_or_queue;
+    if ( $depend_jobnum ) {
+      my $error = $err_or_queue->depend_insert($depend_jobnum);
+      return $error if $error;
+    }
+  }
+  # no other svc_phone changes need to be exported
+  '';
+}
+
+sub _export_suspend {
+  my( $self, $svc_phone ) = (shift, shift);
+  $self->_export_lock($svc_phone, 'TRUE');
+}
+
+sub _export_unsuspend {
+  my( $self, $svc_phone ) = (shift, shift);
+  $self->_export_lock($svc_phone, 'FALSE');
+}
+
+sub _export_lock {
+  my ($self, $svc_phone, $lockstate) = @_;
+  # XXX I'm not sure this actually suspends.  Need to test it.
+  my @command = (
+    'MOD LCK',
+    IMSI    => '"'.$svc_phone->sim_imsi.'"',
+    ISDN    => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"',
+    IC      => $lockstate,
+    OC      => $lockstate,
+    GPRSLOCK=> $lockstate,
+  );
+  my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command);
+  ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+sub _export_delete {
+  my( $self, $svc_phone ) = (shift, shift);
+  my @command = (
+    'RMV SUB',
+    #IMSI    => '"'.$svc_phone->sim_imsi.'"',
+    ISDN    => '"'.$svc_phone->countrycode.$svc_phone->phonenum.'"',
+  );
+  my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command);
+  ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+sub queue_command {
+  my ($self, $svcnum, @command) = @_;
+  my $queue = FS::queue->new({
+      svcnum  => $svcnum,
+      job     => 'FS::part_export::huawei_hlr::run_command',
+  });
+  $queue->insert($self->exportnum, @command) || $queue;
+}
+
+sub run_command {
+  my ($exportnum, @command) = @_;
+  my $self = FS::part_export->by_key($exportnum);
+  my $socket = $self->login;
+  my $result = $self->command($socket, @command);
+  $self->logout($socket);
+  $socket->close;
+  die $result->{error} if $result->{error};
+  '';
+}
+
+sub login {
+  my $self = shift;
+  local $DEBUG = $self->option('debug') || 0;
+  # Send a command to the SMU.
+  # The caller is responsible for quoting string parameters.
+  my %socket_param = (
+    PeerAddr  => $self->machine,
+    PeerPort  => 7777,
+    Proto     => 'tcp',
+    Timeout   => ($self->option('timeout') || 30),
+  );
+  warn "Connecting to ".$self->machine."...\n" if $DEBUG;
+  warn Dumper(\%socket_param) if $DEBUG;
+  my $socket = IO::Socket::INET->new(%socket_param)
+    or die "Failed to connect: $!\n";
+
+  warn 'Logging in as "'.$self->option('opname').".\"\n" if $DEBUG;
+  my @login_param = (
+    OPNAME => '"'.$self->option('opname').'"',
+    PWD    => '"'.$self->option('pwd').'"',
+  );
+  if ($self->option('HLRSN')) {
+    unshift @login_param, 'HLRSN', $self->option('HLRSN');
+  }
+  my $login_result = $self->command($socket, 'LGI', @login_param);
+  die $login_result->{error} if $login_result->{error};
+  return $socket;
+}
+
+sub logout {
+  warn "Logging out.\n" if $DEBUG;
+  my $self = shift;
+  my ($socket) = @_;
+  $self->command($socket, 'LGO');
+  $socket->close;
+}
+
+sub command {
+  my $self = shift;
+  my ($socket, $command, @param) = @_;
+  my $string = $command . ':';
+  while (@param) {
+    $string .= shift(@param) . '=' . shift(@param);
+    $string .= ',' if @param;
+  }
+  $string .= "\n;";
+  my @result;
+  eval { # timeout
+    local $SIG{ALRM} = sub { die "timeout\n" };
+    alarm ($self->option('timeout') || 120);
+    warn "Sending to server:\n$string\n\n" if $DEBUG;
+    $socket->print($string);
+    warn "Received:\n";
+    my $line;
+    local $/ = "\r\n";
+    do {
+      $line = $socket->getline();
+      warn $line if $DEBUG;
+      chomp $line;
+      push @result, $line if length($line);
+    } until ( $line =~ /^---\s*END$/ or $socket->eof );
+    alarm 0;
+  };
+  my %return;
+  if ( $@ eq "timeout\n" ) {
+    return { error => 'request timed out' };
+  } elsif ( $@ ) {
+    return { error => $@ };
+  } else {
+    #+++    HLR9820        <date> <time>\n
+    my $header = shift(@result);
+    $header =~ /(\+\+\+.*)/
+      or return { error => 'malformed response: '.$header };
+    $return{header} = $1;
+    #SMU    #<serial number>\n
+    $return{smu} = shift(@result);
+    #%%<command string>%%\n 
+    $return{echo} = shift(@result); # should match the input
+    #<message code>: <message description>\n
+    my $message = shift(@result);
+    if ($message =~ /^SUCCESS/) {
+      $return{success} = $message;
+    } else { #/^ERR/
+      $return{error} = $message;
+    }
+    $return{trailer} = pop(@result);
+    $return{details} = join("\n",@result,'');
+  }
+  \%return;
+}
+
+sub process_import_sim {
+  my $job = shift;
+  my $param = thaw(decode_base64(shift));
+  $param->{'job'} = $job;
+  my $exportnum = delete $param->{'exportnum'};
+  my $export = __PACKAGE__->by_key($exportnum);
+  my $file = delete $param->{'uploaded_files'};
+  $file =~ s/^file://;
+  my $dir = $FS::UID::cache_dir .'/cache.'. $FS::UID::datasrc;
+  open( $param->{'filehandle'}, '<', "$dir/$file" )
+    or die "unable to open '$file'.\n";
+  my $error = $export->import_sim($param);
+}
+
+sub import_sim {
+  # import a SIM list
+  local $FS::UID::AutoCommit = 1; # yes, 1
+  my $self = shift;
+  my $param = shift;
+  my $job = $param->{'job'};
+  my $fh = $param->{'filehandle'};
+  my @lines = $fh->getlines;
+
+  my @command = 'ADD KI';
+  push @command, ('HLRSN', $self->option('hlrsn')) if $self->option('hlrsn');
+
+  my @args = ('OPERTYPE', 'ADD');
+  push @args, ('K4SNO', $self->option('k4sno')) if $self->option('k4sno');
+  push @args, ('CARDTYPE', $self->option('cardtype'),
+               'ALG',      $self->option('alg'));
+  push @args, ('OPCVALUE', $self->option('opcvalue'),
+               'OPSNO',    $self->option('opsno'))
+    if $self->option('alg') eq 'MILENAGE';
+
+  my $agentnum = $param->{'agentnum'};
+  my $classnum = $param->{'classnum'};
+  my $class = FS::inventory_class->by_key($classnum)
+    or die "bad inventory class $classnum\n";
+  my %existing = map { $_->item, 1 } 
+    qsearch('inventory_item', { 'classnum' => $classnum });
+
+  my $socket = $self->login;
+  my $num=0;
+  my $total = scalar(@lines);
+  foreach my $line (@lines) {
+    $num++;
+    $job->update_statustext(int(100*$num/$total).',Provisioning IMSIs...')
+      if $job;
+
+    chomp $line;
+    my ($imsi, $iccid, $pin1, $puk1, $pin2, $puk2, $acc, $ki) = 
+      split(' ', $line);
+    # the only fields we really care about are the IMSI and KI.
+    if ($imsi !~ /^\d{15}$/ or $ki !~ /^[0-9A-Z]{32}$/) {
+      warn "misspelled line in SIM file: $line\n";
+      next;
+    }
+    if ($existing{$imsi}) {
+      warn "IMSI $imsi already in inventory, skipped\n";
+      next;
+    }
+
+    # push IMSI/KI to the HLR
+    my $return = $self->command($socket,
+      @command,
+      'IMSI', $imsi,
+      'KIVALUE', $ki,
+      @args
+    );
+    if ( $return->{success} ) {
+      # add to inventory
+      my $item = FS::inventory_item->new({
+          'classnum'  => $classnum,
+          'agentnum'  => $agentnum,
+          'item'      => $imsi,
+      });
+      my $error = $item->insert;
+      if ( $error ) {
+        die "IMSI $imsi added to HLR, but not to inventory:\n$error\n";
+      }
+    } else {
+      die "IMSI $imsi could not be added to HLR:\n".$return->{error}."\n";
+    }
+  } #foreach $line
+  $self->logout($socket);
+  return;
+}
+
+1;
index 2e37d04..c72093d 100644 (file)
@@ -72,7 +72,7 @@ tie my %options, 'Tie::IxHash',
 ;
 
 %info = (
-  'svc'        => [ 'svc_phone', ], # 'part_device',
+  'svc'        => [qw( svc_phone part_device )],
   'desc'       => 'Provision phone numbers to NetSapiens',
   'options'    => \%options,
   'no_machine' => 1,
index 9ace213..161ffe0 100644 (file)
@@ -13,16 +13,18 @@ use FS::part_export;
 #- suspension/unsuspension
 
 tie my %options, 'Tie::IxHash',
-  'user'      => { label=>'Remote username', default=>'root', },
-  'useradd'   => { label=>'Insert command', }, 
-  'userdel'   => { label=>'Delete command', }, 
-  'usermod'   => { label=>'Modify command', }, 
-  'suspend'   => { label=>'Suspension command', }, 
-  'unsuspend' => { label=>'Unsuspension command', }, 
+  'user'       => { label=>'Remote username', default=>'root', },
+  'useradd'    => { label=>'Insert command', }, 
+  'userdel'    => { label=>'Delete command', }, 
+  'usermod'    => { label=>'Modify command', }, 
+  'suspend'    => { label=>'Suspension command', }, 
+  'unsuspend'  => { label=>'Unsuspension command', }, 
+  'mac_insert' => { label=>'Device MAC address insert command', },
+  'mac_delete' => { label=>'Device MAC address delete command', },
 ;
 
 %info = (
-  'svc'     => 'svc_phone',
+  'svc'     => [qw( svc_phone part_device )],
   'desc'    => 'Run remote commands via SSH, for phone numbers',
   'options' => \%options,
   'notes'   => <<'END'
@@ -50,6 +52,7 @@ old_ for replace operations):
   <LI><code>$pin</code> - Personal identification number
   <LI><code>$cust_name</code> - Customer name (quoted for the shell)
   <LI><code>$pkgnum</code> - Internal package number
+  <LI><code>$mac_addr</code> - MAC address (Device MAC address insert and delete commands only)
 </UL>
 END
 );
@@ -57,27 +60,41 @@ END
 sub rebless { shift; }
 
 sub _export_insert {
-  my($self) = shift;
+  my $self = shift;
   $self->_export_command('useradd', @_);
 }
 
 sub _export_delete {
-  my($self) = shift;
+  my $self = shift;
   $self->_export_command('userdel', @_);
 }
 
 sub _export_suspend {
-  my($self) = shift;
+  my $self = shift;
   $self->_export_command('suspend', @_);
 }
 
 sub _export_unsuspend {
-  my($self) = shift;
+  my $self = shift;
   $self->_export_command('unsuspend', @_);
 }
 
+sub export_device_insert {
+  my( $self, $svc_phone, $phone_device ) = @_;
+  $self->_export_command('mac_insert', $svc_phone,
+                           'mac_addr'=>$phone_device->mac_addr
+                        );
+}
+
+sub export_device_delete {
+  my( $self, $svc_phone, $phone_device ) = @_;
+  $self->_export_command('mac_delete', $svc_phone,
+                           'mac_addr'=>$phone_device->mac_addr
+                        );
+}
+
 sub _export_command {
-  my ( $self, $action, $svc_phone) = (shift, shift, shift);
+  my ( $self, $action, $svc_phone, %addl_vars) = @_;
   my $command = $self->option($action);
   return '' if $command =~ /^\s*$/;
 
@@ -86,6 +103,7 @@ sub _export_command {
   {
     no strict 'refs';
     ${$_} = $svc_phone->getfield($_) foreach $svc_phone->fields;
+    ${$_} = $addl_vars{$_} foreach keys %addl_vars;
   }
   my $cust_pkg = $svc_phone->cust_svc->cust_pkg;
   my $pkgnum = $cust_pkg ? $cust_pkg->pkgnum : '';
index f964af3..9408d14 100644 (file)
@@ -243,12 +243,12 @@ sub _export_command {
     ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
 
     # snarfs are unused at this point?
-    my $count = 1;
-    foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
-      ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
-        foreach qw( machine username _password );
-      $count++;
-    }
+    my $count = 1;
+    foreach my $acct_snarf ( $svc_acct->acct_snarf ) {
+      ${"snarf_$_$count"} = shell_quote( $acct_snarf->get($_) )
+        foreach qw( machine username _password );
+      $count++;
+    }
   }
 
   my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
index 58cc5be..833dd9a 100644 (file)
@@ -597,7 +597,8 @@ New-style: pass a hashref with the following keys:
 
 =item stoptime_end - Upper bound for AcctStopTime, as a UNIX timestamp
 
-=item open_sessions - Only show records with no AcctStopTime (typically used without stoptime_* options and with starttime_* options instead)
+=item session_status - 'closed' to only show records with AcctStopTime,
+'open' to only show records I<without> AcctStopTime, empty to show both.
 
 =item starttime_start - Lower bound for AcctStartTime, as a UNIX timestamp
 
@@ -727,17 +728,27 @@ sub usage_sessions {
     push @where, " CalledStationID LIKE 'sip:$prefix\%'";
   }
 
-  if ( $start ) {
-    push @where, "$str2time AcctStopTime ) >= ?";
-    push @param, $start;
-  }
-  if ( $end ) {
-    push @where, "$str2time AcctStopTime ) <= ?";
-    push @param, $end;
+  my $acctstoptime = '';
+  if ( $opt->{session_status} ne 'open' ) {
+    if ( $start ) {
+      $acctstoptime .= "$str2time AcctStopTime ) >= ?";
+      push @param, $start;
+      $acctstoptime .= ' AND ' if $end;
+    }
+    if ( $end ) {
+      $acctstoptime .= "$str2time AcctStopTime ) <= ?";
+      push @param, $end;
+    }
   }
-  if ( $opt->{open_sessions} ) {
-    push @where, 'AcctStopTime IS NULL';
+  if ( $opt->{session_status} ne 'closed' ) {
+    if ( $acctstoptime ) {
+      $acctstoptime = "( ( $acctstoptime ) OR AcctStopTime IS NULL )";
+    } else {
+      $acctstoptime = 'AcctStopTime IS NULL';
+    }
   }
+  push @where, $acctstoptime;
+
   if ( $opt->{starttime_start} ) {
     push @where, "$str2time AcctStartTime ) >= ?";
     push @param, $opt->{starttime_start};
@@ -756,10 +767,14 @@ sub usage_sessions {
   my $orderby = 'ORDER BY AcctStartTime DESC';
   $orderby = '' if $summarize;
 
-  my $sth = $dbh->prepare('SELECT '. join(', ', @fields).
-                          "  FROM radacct $where $groupby $orderby
-                        ") or die $dbh->errstr;                                 
-  $sth->execute(@param) or die $sth->errstr;
+  my $sql = 'SELECT '. join(', ', @fields).
+            "  FROM radacct $where $groupby $orderby";
+  if ( $DEBUG ) {
+    warn $sql;
+    warn join(',', @param);
+  }
+  my $sth = $dbh->prepare($sql) or die $dbh->errstr;
+  $sth->execute(@param)         or die $sth->errstr;
 
   [ map { { %$_ } } @{ $sth->fetchall_arrayref({}) } ];
 
index 53d2b37..c5200ec 100644 (file)
@@ -43,6 +43,10 @@ sub _export_unsuspend {}
 sub export_setstatus {
   my($self, $svc_acct, $hashref) = @_;
 
+  for (qw( spam_tag2_level spam_kill_level )) {
+    $hashref->{$_} =~ /^\d+(\.\d+)?$/ or return "illegal $_";
+  }
+
   my @shellargs = (
     $svc_acct->svcnum,
     user          => $self->option('user') || 'root',
diff --git a/FS/FS/part_export/test.pm b/FS/FS/part_export/test.pm
new file mode 100644 (file)
index 0000000..126897c
--- /dev/null
@@ -0,0 +1,75 @@
+package FS::part_export::test;
+
+use strict;
+use vars qw(%options %info);
+use Tie::IxHash;
+use base qw(FS::part_export);
+
+tie %options, 'Tie::IxHash',
+  'result'  => { label    => 'Result',
+                 type     => 'select',
+                 options  => [ 'success', 'failure', 'exception' ],
+                 default  => 'success',
+               },
+  'errormsg'=> { label    => 'Error message',
+                 default  => 'Test export' },
+  'insert'  => { label    => 'Insert', type => 'checkbox', default => 1, },
+  'delete'  => { label    => 'Delete', type => 'checkbox', default => 1, },
+  'replace' => { label    => 'Replace',type => 'checkbox', default => 1, },
+  'suspend' => { label    => 'Suspend',type => 'checkbox', default => 1, },
+  'unsuspend'=>{ label => 'Unsuspend', type => 'checkbox', default => 1, },
+;
+
+%info = (
+  'svc'     => [ qw(svc_acct svc_broadband svc_phone svc_domain) ],
+  'desc'    => 'Test export for development',
+  'options' => \%options,
+  'notes'   => <<END,
+<P>Test export.  Do not use this in production systems.</P>
+<P>This export either always succeeds, always fails (returning an error),
+or always dies, according to the "Result" option.  It does nothing else; the
+purpose is purely to simulate success or failure within an export module.</P>
+<P>The checkbox options can be used to turn the export off for certain
+actions, if this is needed.</P>
+END
+);
+
+sub export_insert {
+  my $self = shift;
+  $self->run(@_) if $self->option('insert');
+}
+
+sub export_delete {
+  my $self = shift;
+  $self->run(@_) if $self->option('delete');
+}
+
+sub export_replace {
+  my $self = shift;
+  $self->run(@_) if $self->option('replace');
+}
+
+sub export_suspend {
+  my $self = shift;
+  $self->run(@_) if $self->option('suspend');
+}
+
+sub export_unsuspend {
+  my $self = shift;
+  $self->run(@_) if $self->option('unsuspend');
+}
+
+sub run {
+  my $self = shift;
+  my $svc_x = shift;
+  my $result = $self->option('result');
+  if ( $result eq 'failure' ) {
+    return $self->option('errormsg');
+  } elsif ( $result eq 'exception' ) {
+    die $self->option('errormsg');
+  } else {
+    return '';
+  }
+}
+
+1;
index 6e7f8f8..e788269 100644 (file)
@@ -1,7 +1,8 @@
 package FS::part_pkg;
+use base qw( FS::m2m_Common FS::o2m_Common FS::option_Common );
 
 use strict;
-use vars qw( @ISA %plans $DEBUG $setup_hack $skip_pkg_svc_hack );
+use vars qw( %plans $DEBUG $setup_hack $skip_pkg_svc_hack );
 use Carp qw(carp cluck confess);
 use Scalar::Util qw( blessed );
 use Time::Local qw( timelocal_nocheck );
@@ -16,14 +17,15 @@ use FS::type_pkgs;
 use FS::part_pkg_option;
 use FS::pkg_class;
 use FS::agent;
+use FS::part_pkg_msgcat;
 use FS::part_pkg_taxrate;
 use FS::part_pkg_taxoverride;
 use FS::part_pkg_taxproduct;
 use FS::part_pkg_link;
 use FS::part_pkg_discount;
+use FS::part_pkg_usage;
 use FS::part_pkg_vendor;
 
-@ISA = qw( FS::m2m_Common FS::option_Common );
 $DEBUG = 0;
 $setup_hack = 0;
 $skip_pkg_svc_hack = 0;
@@ -364,7 +366,7 @@ sub replace {
       ? shift
       : { @_ };
 
-  $options->{options} = {} unless defined($options->{options});
+  $options->{options} = { $old->options } unless defined($options->{options});
 
   warn "FS::part_pkg::replace called on $new to replace $old with options".
        join(', ', map "$_ => ". $options->{$_}, keys %$options)
@@ -446,53 +448,55 @@ sub replace {
   }
 
   warn "  replacing pkg_svc records" if $DEBUG;
-  my $pkg_svc = $options->{'pkg_svc'} || {};
+  my $pkg_svc = $options->{'pkg_svc'};
   my $hidden_svc = $options->{'hidden_svc'} || {};
-  foreach my $part_svc ( qsearch('part_svc', {} ) ) {
-    my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
-    my $hidden = $hidden_svc->{$part_svc->svcpart} || '';
-    my $primary_svc =
-      ( defined($options->{'primary_svc'}) && $options->{'primary_svc'}
-        && $options->{'primary_svc'} == $part_svc->svcpart
-      )
-        ? 'Y'
-        : '';
+  if ( $pkg_svc ) { # if it wasn't passed, don't change existing pkg_svcs
+    foreach my $part_svc ( qsearch('part_svc', {} ) ) {
+      my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
+      my $hidden = $hidden_svc->{$part_svc->svcpart} || '';
+      my $primary_svc =
+        ( defined($options->{'primary_svc'}) && $options->{'primary_svc'}
+          && $options->{'primary_svc'} == $part_svc->svcpart
+        )
+          ? 'Y'
+          : '';
 
-    my $old_pkg_svc = qsearchs('pkg_svc', {
-        'pkgpart' => $old->pkgpart,
-        'svcpart' => $part_svc->svcpart,
+      my $old_pkg_svc = qsearchs('pkg_svc', {
+          'pkgpart' => $old->pkgpart,
+          'svcpart' => $part_svc->svcpart,
+        }
+      );
+      my $old_quantity = 0;
+      my $old_primary_svc = '';
+      my $old_hidden = '';
+      if ( $old_pkg_svc ) {
+        $old_quantity = $old_pkg_svc->quantity;
+        $old_primary_svc = $old_pkg_svc->primary_svc 
+          if $old_pkg_svc->dbdef_table->column('primary_svc'); # is this needed?
+        $old_hidden = $old_pkg_svc->hidden;
       }
-    );
-    my $old_quantity = 0;
-    my $old_primary_svc = '';
-    my $old_hidden = '';
-    if ( $old_pkg_svc ) {
-      $old_quantity = $old_pkg_svc->quantity;
-      $old_primary_svc = $old_pkg_svc->primary_svc 
-        if $old_pkg_svc->dbdef_table->column('primary_svc'); # is this needed?
-      $old_hidden = $old_pkg_svc->hidden;
-    }
-    next unless $old_quantity != $quantity || 
-                $old_primary_svc ne $primary_svc ||
-                $old_hidden ne $hidden;
-  
-    my $new_pkg_svc = new FS::pkg_svc( {
-      'pkgsvcnum'   => ( $old_pkg_svc ? $old_pkg_svc->pkgsvcnum : '' ),
-      'pkgpart'     => $new->pkgpart,
-      'svcpart'     => $part_svc->svcpart,
-      'quantity'    => $quantity, 
-      'primary_svc' => $primary_svc,
-      'hidden'      => $hidden,
-    } );
-    my $error = $old_pkg_svc
-                  ? $new_pkg_svc->replace($old_pkg_svc)
-                  : $new_pkg_svc->insert;
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return $error;
-    }
-  }
+   
+      next unless $old_quantity != $quantity || 
+                  $old_primary_svc ne $primary_svc ||
+                  $old_hidden ne $hidden;
+    
+      my $new_pkg_svc = new FS::pkg_svc( {
+        'pkgsvcnum'   => ( $old_pkg_svc ? $old_pkg_svc->pkgsvcnum : '' ),
+        'pkgpart'     => $new->pkgpart,
+        'svcpart'     => $part_svc->svcpart,
+        'quantity'    => $quantity, 
+        'primary_svc' => $primary_svc,
+        'hidden'      => $hidden,
+      } );
+      my $error = $old_pkg_svc
+                    ? $new_pkg_svc->replace($old_pkg_svc)
+                    : $new_pkg_svc->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    } #foreach $part_svc
+  } #if $options->{pkg_svc}
   
   my @part_pkg_vendor = $old->part_pkg_vendor;
   my @current_exportnum = ();
@@ -712,6 +716,35 @@ sub propagate {
   join("\n", @error);
 }
 
+=item pkg_locale LOCALE
+
+Returns a customer-viewable string representing this package for the given
+locale, from the part_pkg_msgcat table.  If the given locale is empty or no
+localized string is found, returns the base pkg field.
+
+=cut
+
+sub pkg_locale {
+  my( $self, $locale ) = @_;
+  return $self->pkg unless $locale;
+  my $part_pkg_msgcat = $self->part_pkg_msgcat($locale) or return $self->pkg;
+  $part_pkg_msgcat->pkg;
+}
+
+=item part_pkg_msgcat LOCALE
+
+Like pkg_locale, but returns the FS::part_pkg_msgcat object itself.
+
+=cut
+
+sub part_pkg_msgcat {
+  my( $self, $locale ) = @_;
+  qsearchs( 'part_pkg_msgcat', {
+    pkgpart => $self->pkgpart,
+    locale  => $locale,
+  });
+}
+
 =item pkg_comment [ OPTION => VALUE... ]
 
 Returns an (internal) string representing this package.  Currently,
@@ -1175,6 +1208,17 @@ sub svc_part_pkg_link {
   shift->_part_pkg_link('svc', @_);
 }
 
+=item supp_part_pkg_link
+
+Returns the associated part_pkg_link records of type 'supp' (supplemental
+packages).
+
+=cut
+
+sub supp_part_pkg_link {
+  shift->_part_pkg_link('supp', @_);
+}
+
 sub _part_pkg_link {
   my( $self, $type ) = @_;
   qsearch({ table    => 'part_pkg_link',
@@ -1384,6 +1428,18 @@ sub part_pkg_discount {
   qsearch('part_pkg_discount', { 'pkgpart' => $self->pkgpart });
 }
 
+=item part_pkg_usage
+
+Returns the voice usage pools (see L<FS::part_pkg_usage>) defined for 
+this package.
+
+=cut
+
+sub part_pkg_usage {
+  my $self = shift;
+  qsearch('part_pkg_usage', { 'pkgpart' => $self->pkgpart });
+}
+
 =item _rebless
 
 Reblesses the object into the FS::part_pkg::PLAN class (if available), where
@@ -1439,6 +1495,29 @@ sub recur_cost_permonth {
   sprintf('%.2f', $self->recur_cost / $self->freq );
 }
 
+=item cust_bill_pkg_recur CUST_PKG
+
+Actual recurring charge for the specified customer package from customer's most
+recent invoice
+
+=cut
+
+sub cust_bill_pkg_recur {
+  my($self, $cust_pkg) = @_;
+  my $cust_bill_pkg = qsearchs({
+    'table'     => 'cust_bill_pkg',
+    'addl_from' => 'LEFT JOIN cust_bill USING ( invnum )',
+    'hashref'   => { 'pkgnum' => $cust_pkg->pkgnum,
+                     'recur'  => { op=>'>', value=>'0' },
+                   },
+    'order_by'  => 'ORDER BY cust_bill._date     DESC,
+                             cust_bill_pkg.sdate DESC
+                     LIMIT 1
+                   ',
+  }) or return 0; #die "use cust_bill_pkg_recur credits with once_perinv condition";
+  $cust_bill_pkg->recur;
+}
+
 =item format OPTION DATA
 
 Returns data formatted according to the function 'format' described
diff --git a/FS/FS/part_pkg/base_delayed.pm b/FS/FS/part_pkg/base_delayed.pm
deleted file mode 100644 (file)
index c6864a6..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-package FS::part_pkg::base_delayed;
-
-use strict;
-use vars qw(@ISA %info);
-#use FS::Record qw(qsearch qsearchs);
-use FS::part_pkg::base_rate;
-
-@ISA = qw(FS::part_pkg::base_rate);
-
-%info = (
-  'name' => 'Free (or setup fee) for X days, then base rate'.
-            ' (anniversary billing)',
-  'shortname' => 'Bulk (manual from "units" option), w/intro period',
-  'inherit_fields' => [ 'global_Mixin' ],
-  'fields' =>  {
-    'free_days' => { 'name' => 'Initial free days',
-                     'default' => 0,
-                   },
-    'recur_notify' => { 'name' => 'Number of days before recurring billing'.
-                                  ' commences to notify customer. (0 means'.
-                                  ' no warning)',
-                     'default' => 0,
-                    },
-  },
-  'fieldorder' => [ 'free_days', 'recur_notify',
-                  ],
-  #'setup' => '\'my $d = $cust_pkg->bill || $time; $d += 86400 * \' + what.free_days.value + \'; $cust_pkg->bill($d); $cust_pkg_mod_flag=1; \' + what.setup_fee.value',
-  #'recur' => 'what.recur_fee.value',
-  'weight' => 54, #&g!
-);
-
-sub calc_setup {
-  my($self, $cust_pkg, $time ) = @_;
-
-  my $d = $cust_pkg->bill || $time;
-  $d += 86400 * $self->option('free_days');
-  $cust_pkg->bill($d);
-  
-  $self->option('setup_fee');
-}
-
-1;
diff --git a/FS/FS/part_pkg/base_rate.pm b/FS/FS/part_pkg/base_rate.pm
deleted file mode 100644 (file)
index 43a0506..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-package FS::part_pkg::base_rate;
-
-use strict;
-use vars qw(@ISA %info);
-#use FS::Record qw(qsearch);
-use FS::part_pkg;
-
-@ISA = qw(FS::part_pkg);
-
-%info = (
-  'name' => 'Base rate (anniversary billing, Times units ordered)',
-            # XXX it multiplies recurring fee by cust_pkg option "units", how to
-            # express that
-  'shortname' => 'Bulk (manual from "units" option)',
-  'inherit_fields' => [ 'global_Mixin' ],
-  'fields' => {
-    'externalid' => { 'name'    => 'Optional External ID',
-                      'default' => '',
-                    },
-  },
-  'fieldorder' => [ qw( externalid ) ],
-  'weight' => 52,
-);
-
-sub price_info {
-    my $self = shift;
-    my $conf = new FS::Conf;
-    my $money_char = $conf->config('money_char') || '$';
-    my $setup = $self->option('setup_fee') || 0;
-    my $recur = $self->option('recur_fee', 1) || 0;
-    my $str = '';
-    $str = $money_char . $setup . ' one-time' if $setup;
-    $str .= ', ' if ($setup && $recur);
-    $str .= $money_char . $recur . ' recurring per unit ' if $recur;
-    $str;
-}
-
-
-sub calc_setup {
-  my($self, $cust_pkg, $sdate, $details ) = @_;
-
-  my $i = 0;
-  my $count = $self->option( 'additional_count', 'quiet' ) || 0;
-  while ($i < $count) {
-    push @$details, $self->option( 'additional_info' . $i++ );
-  }
-
-  $self->option('setup_fee');
-}
-
-sub calc_recur {
-  my($self, $cust_pkg) = @_;
-  $self->base_recur($cust_pkg);
-}
-
-sub base_recur {
-  my($self, $cust_pkg) = @_;
-  my $units = $cust_pkg->option('units') ? $cust_pkg->option('units') : 1 ;
-       # default to 1 if not found
-  sprintf("%.2f", 
-         ($self->option('recur_fee') * $units ) 
-  );
-}
-
-sub calc_remain {
-  my ($self, $cust_pkg, %options) = @_;
-  my $time = $options{'time'} || time;
-  my $next_bill = $cust_pkg->getfield('bill') || 0;
-  return 0 if  ! $self->base_recur($cust_pkg)
-              || ! $next_bill
-              || $next_bill < $time;
-
-  my %sec = (
-    'h' =>    3600, # 60 * 60
-    'd' =>   86400, # 60 * 60 * 24
-    'w' =>  604800, # 60 * 60 * 24 * 7
-    'm' => 2629744, # 60 * 60 * 24 * 365.2422 / 12 
-  );
-
-  $self->freq =~ /^(\d+)([hdwm]?)$/
-    or die 'unparsable frequency: '. $self->freq;
-  my $freq_sec = $1 * $sec{$2||'m'};
-  return 0 unless $freq_sec;
-
-  sprintf("%.2f", $self->base_recur($cust_pkg) * ( $next_bill - $time ) / $freq_sec );
-
-}
-
-sub is_free_options {
-  qw( setup_fee recur_fee );
-}
-
-sub is_prepaid {
-  0; #no, we're postpaid
-}
-
-1;
index 83e543a..ab53bda 100644 (file)
@@ -23,7 +23,8 @@ use NEXT;
 );
 
 sub calc_setup {
-  my($self, $cust_pkg, $time ) = @_;
+  my $self = shift;
+  my( $cust_pkg, $time ) = @_;
 
   unless ( $self->option('delay_setup', 1) ) {
     my $d = $cust_pkg->bill || $time;
@@ -31,7 +32,7 @@ sub calc_setup {
     $cust_pkg->bill($d);
   }
   
-  $self->option('setup_fee');
+  $self->NEXT::calc_setup(@_);
 }
 
 sub calc_remain {
index 10c2056..7337602 100644 (file)
@@ -1,12 +1,8 @@
 package FS::part_pkg::flat_introrate;
+use base qw( FS::part_pkg::flat );
 
 use strict;
-use vars qw(@ISA %info $DEBUG $me);
-use FS::part_pkg::flat;
-
-@ISA = qw(FS::part_pkg::flat);
-$me = '[' . __PACKAGE__ . ']';
-$DEBUG = 0;
+use vars qw( %info );
 
 %info = (
   'name' => 'Introductory price for X months, then flat rate,'.
index d148c96..9efc7e8 100644 (file)
@@ -67,11 +67,11 @@ the base price per billing cycle.
 
 Options:
 - add_full_period: Bill for the time up to the prorate day plus one full
-billing period after that.
+  billing period after that.
 - prorate_round_day: Round the current time to the nearest full day, 
-instead of using the exact time.
+  instead of using the exact time.
 - prorate_defer_bill: Don't bill the prorate interval until the prorate 
-day arrives.
+  day arrives.
 - prorate_verbose: Generate details to explain the prorate calculations.
 
 =cut
@@ -104,7 +104,7 @@ sub calc_prorate {
     $add_period = 1;
   }
 
-  # if the customer alreqady has a billing day-of-month established,
+  # if the customer already has a billing day-of-month established,
   # and it's a valid cutoff day, try to respect it
   my $next_bill_day;
   if ( my $next_bill = $cust_pkg->cust_main->next_bill_date ) {
@@ -123,38 +123,56 @@ sub calc_prorate {
 
   my $permonth = $charge / $self->freq;
   my $months = ( ( $self->freq - 1 ) + ($mend-$mnow) / ($mend-$mstart) );
-
-  if ( $self->option('prorate_verbose',1) 
-      and $months > 0 and $months < $self->freq ) {
-    push @$details, 
-          'Prorated (' . time2str('%b %d', $mnow) .
-            ' - ' . time2str('%b %d', $mend) . '): ' . $money_char . 
-            sprintf('%.2f', $permonth * $months + 0.00000001 );
-  }
+  # after this, $self->freq - 1 < $months <= $self->freq
 
   # add a full period if currently billing for a partial period
   # or periods up to freq_override if billing for an override interval
   if ( ($param->{'freq_override'} || 0) > 1 ) {
     $months += $param->{'freq_override'} - 1;
-  } 
-  elsif ( $add_period && $months < $self->freq) {
+    # freq_override - 1 correct here?
+    # (probably only if freq == 1, yes?)
+  } elsif ( $add_period && $months < $self->freq ) {
 
-    if ( $self->option('prorate_verbose',1) ) {
-      # calculate the prorated and add'l period charges
+    # 'add_period' is a misnomer.
+    # we add enough to make the total at least a full period
+    $months++;
+    $$sdate = $self->add_freq($mstart, 1);
+    # now $self->freq <= $months <= $self->freq + 1
+    # (note that this only happens if $months < $self->freq to begin with)
+
+  }
+
+  if ( $self->option('prorate_verbose',1) and $months > 0 ) {
+    if ( $months < $self->freq ) {
+      # we are billing a fractional period only
+      #       # (though maybe not a fractional month)
+      my $period_end = $self->add_freq($mstart);
+      push @$details, 
+      'Prorated (' . time2str('%b %d', $mnow) .
+      ' - ' . time2str('%b %d', $period_end) . '): ' . $money_char .
+      sprintf('%.2f', $permonth * $months + 0.00000001 );
+
+    } elsif ( $months > $self->freq ) {
+      # we are billing MORE than a full period
       push @$details,
-        'First full month: ' . $money_char . 
-          sprintf('%.2f', $permonth);
-    }
 
-    $months += $self->freq;
-    $$sdate = $self->add_freq($mstart);
+      'Prorated (' . time2str('%b %d', $mnow) .
+      ' - ' . time2str('%b %d', $mend) . '): ' . $money_char .
+      sprintf('%.2f', $permonth * ($months - $self->freq + 0.0000001)),
+
+      'First full period: ' . $money_char .
+      sprintf('%.2f', $permonth * $self->freq);
+    } # else $months == $self->freq, and no prorating has happened
   }
 
   $param->{'months'} = $months;
                                                   #so 1.005 rounds to 1.01
   $charge = sprintf('%.2f', $permonth * $months + 0.00000001 );
 
-  return $charge;
+  my $quantity = $cust_pkg->quantity || 1;
+  $charge *= $quantity;
+
+  return sprintf('%.2f', $charge);
 }
 
 =item prorate_setup CUST_PKG SDATE
index aae51e9..21c6a8a 100644 (file)
@@ -51,6 +51,11 @@ tie my %unrateable_opts, 'Tie::IxHash',
   2  => 'Flag for later review',
 ;
 
+tie my %detail_formats, 'Tie::IxHash',
+  '' => '',
+  FS::cdr::invoice_formats()
+;
+
 %info = (
   'name' => 'VoIP rating by plan of CDR records in an internal (or external) SQL table',
   'shortname' => 'VoIP/telco CDR rating (standard)',
@@ -152,10 +157,16 @@ tie my %unrateable_opts, 'Tie::IxHash',
     'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: ',
                          },
 
-    'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to: ',
+    'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to this cdrtypenum: ',
+                         },
+    
+    'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to this cdrtypenum: ',
+                         },
+
+    'use_calltypenum' => { 'name' => 'Only charge for CDRs where the CDR Call Type is set to this calltypenum: ',
                          },
     
-    'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to: ',
+    'ignore_calltypenum' => { 'name' => 'Do not charge for CDRs where the CDR Call Type is set to this calltypenum: ',
                          },
     
     'ignore_disposition' => { 'name' => 'Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values: ',
@@ -203,6 +214,11 @@ tie my %unrateable_opts, 'Tie::IxHash',
     'skip_max_callers' => { 'name' => 'Do not charge for CDRs where max_callers is less than or equal to this value: ',
                           },
 
+    'skip_same_customer' => {
+      'name' => 'Do not charge for calls between numbers belonging to the same customer',
+      'type' => 'checkbox',
+    },
+
     'use_duration'   => { 'name' => 'Calculate usage based on the duration field instead of the billsec field',
                           'type' => 'checkbox',
                         },
@@ -211,12 +227,25 @@ tie my %unrateable_opts, 'Tie::IxHash',
                       },
 
     #false laziness w/cdr_termination.pm
-    'output_format' => { 'name' => 'CDR invoice display format',
+    'output_format' => { 'name' => 'CDR display format for invoices',
                          'type' => 'select',
-                         'select_options' => { FS::cdr::invoice_formats() },
+                         'select_options' => \%detail_formats,
                          'default'        => 'default', #XXX test
                        },
 
+    'selfservice_format' => 
+      { 'name' => 'CDR display format for selfservice',
+        'type' => 'select',
+        'select_options' => \%detail_formats,
+        'default' => 'default'
+      },
+    'selfservice_inbound_format' =>
+      { 'name' => 'Inbound CDR display format for selfservice',
+        'type' => 'select',
+        'select_options' => \%detail_formats,
+        'default' => ''
+      },
+
     'usage_section' => { 'name' => 'Section in which to place usage charges (whether separated or not): ',
                        },
 
@@ -286,6 +315,7 @@ tie my %unrateable_opts, 'Tie::IxHash',
                        use_amaflags
                        use_carrierid 
                        use_cdrtypenum ignore_cdrtypenum
+                       use_calltypenum ignore_calltypenum
                        ignore_disposition disposition_in
                        skip_dcontext skip_dst_prefix 
                        skip_dstchannel_prefix skip_src_length_more 
@@ -295,9 +325,12 @@ tie my %unrateable_opts, 'Tie::IxHash',
                        noskip_dst_length_accountcode_tollfree
                        skip_lastapp
                        skip_max_callers
+                       skip_same_customer
                        use_duration
                        411_rewrite
-                       output_format usage_mandate summarize_usage usage_section
+                       output_format 
+                       selfservice_format selfservice_inbound_format
+                       usage_mandate summarize_usage usage_section
                        bill_every_call bill_inactive_svcs
                        count_available_phones suspend_bill 
                      )
@@ -394,6 +427,7 @@ sub calc_usage {
         'disable_src'    => $self->option('disable_src'),
         'default_prefix' => $self->option('default_prefix'),
         'cdrtypenum'     => $self->option('use_cdrtypenum'),
+        'calltypenum'    => $self->option('use_calltypenum'),
         'status'         => '',
         'for_update'     => 1,
       );  # $last_bill, $$sdate )
@@ -414,6 +448,7 @@ sub calc_usage {
 
       my $error = $cdr->rate(
         'part_pkg'                          => $self,
+        'cust_pkg'                          => $cust_pkg,
         'svcnum'                            => $svc_x->svcnum,
         'single_price_included_min'         => \$included_min,
         'region_group_included_min'         => \$region_group_included_min,
@@ -460,6 +495,7 @@ sub calc_usage {
 }
 
 #returns a reason why not to rate this CDR, or false if the CDR is chargeable
+# lots of false laziness w/voip_inbound
 sub check_chargable {
   my( $self, $cdr, %flags ) = @_;
 
@@ -493,6 +529,15 @@ sub check_chargable {
     if length($self->option_cacheable('ignore_cdrtypenum'))
     && $cdr->cdrtypenum eq $self->option_cacheable('ignore_cdrtypenum'); #eq otherwise 0 matches ''
 
+  # unlike everything else, use_calltypenum is applied in FS::svc_x::get_cdrs.
+  return "calltypenum != ". $self->option_cacheable('use_calltypenum')
+    if length($self->option_cacheable('use_calltypenum'))
+    && $cdr->calltypenum ne $self->option_cacheable('use_calltypenum'); #ne otherwise 0 matches ''
+  
+  return "calltypenum == ". $self->option_cacheable('ignore_calltypenum')
+    if length($self->option_cacheable('ignore_calltypenum'))
+    && $cdr->calltypenum eq $self->option_cacheable('ignore_calltypenum'); #eq otherwise 0 matches ''
+
   return "dcontext IN ( ". $self->option_cacheable('skip_dcontext'). " )"
     if $self->option_cacheable('skip_dcontext') =~ /\S/
     && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext'));
@@ -561,6 +606,41 @@ sub calc_units {
   $count;
 }
 
+sub reset_usage {
+  my ($self, $cust_pkg, %opt) = @_;
+  my @part_pkg_usage = $self->part_pkg_usage or return '';
+  warn "  resetting usage minutes\n" if $opt{debug};
+  my %cust_pkg_usage = map { $_->pkgusagepart, $_ } $cust_pkg->cust_pkg_usage;
+  foreach my $part_pkg_usage (@part_pkg_usage) {
+    my $part = $part_pkg_usage->pkgusagepart;
+    my $usage = $cust_pkg_usage{$part} ||
+                FS::cust_pkg_usage->new({
+                    'pkgnum'        => $cust_pkg->pkgnum,
+                    'pkgusagepart'  => $part,
+                    'minutes'       => $part_pkg_usage->minutes,
+                });
+    foreach my $cdr_usage (
+      qsearch('cdr_cust_pkg_usage', {'cdrusagenum' => $usage->cdrusagenum})
+    ) {
+      my $error = $cdr_usage->delete;
+      warn "  error resetting CDR usage: $error\n";
+    }
+
+    if ( $usage->pkgusagenum ) {
+      if ( $part_pkg_usage->rollover ) {
+        $usage->set('minutes', $part_pkg_usage->minutes + $usage->minutes);
+      } else {
+        $usage->set('minutes', $part_pkg_usage->minutes);
+      }
+      my $error = $usage->replace;
+      warn "  error resetting usage minutes: $error\n" if $error;
+    } else {
+      my $error = $usage->insert;
+      warn "  error resetting usage minutes: $error\n" if $error;
+    }
+  } #foreach $part_pkg_usage
+}
+
 # tells whether cust_bill_pkg_detail should return a single line for 
 # each phonenum
 sub sum_usage {
index 9054f7b..525db80 100644 (file)
@@ -60,15 +60,21 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities();
                         'type' => 'checkbox',
                       },
 
-    'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to: ',
+    'use_carrierid' => { 'name' => 'Only charge for CDRs where the Carrier ID is set to any of these (comma-separated) values: ',
                          },
 
-    'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to: ',
+    'use_cdrtypenum' => { 'name' => 'Only charge for CDRs where the CDR Type is set to this cdrtypenum: ',
                          },
     
-    'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to: ',
+    'ignore_cdrtypenum' => { 'name' => 'Do not charge for CDRs where the CDR Type is set to this cdrtypenum: ',
                          },
 
+    'use_calltypenum' => { 'name' => 'Only charge for CDRs where the CDR Call Type is set to this cdrtypenum: ',
+                         },
+    
+    'ignore_calltypenum' => { 'name' => 'Do not charge for CDRs where the CDR Call Type is set to this cdrtypenum: ',
+                         },
+    
     'ignore_disposition' => { 'name' => 'Do not charge for CDRs where the Disposition is set to any of these (comma-separated) values: ',
                          },
     
@@ -147,6 +153,7 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities();
                        use_amaflags
                        use_carrierid
                        use_cdrtypenum ignore_cdrtypenum
+                       use_calltypenum ignore_calltypenum
                        ignore_disposition disposition_in
                        skip_dcontext skip_dstchannel_prefix
                        skip_dst_length_less skip_lastapp
@@ -329,67 +336,58 @@ sub calc_usage {
 }
 
 #returns a reason why not to rate this CDR, or false if the CDR is chargeable
+# lots of false laziness w/voip_cdr...
 sub check_chargable {
   my( $self, $cdr, %flags ) = @_;
 
-  #should have some better way of checking these options from a hash
-  #or something
-
-  my @opt = qw(
-    use_amaflags
-    use_carrierid
-    use_cdrtypenum
-    ignore_cdrtypenum
-    disposition_in
-    ignore_disposition
-    skip_dcontext
-    skip_dstchannel_prefix
-    skip_dst_length_less
-    skip_lastapp
-  );
-  foreach my $opt (grep !exists($flags{option_cache}->{$_}), @opt ) {
-    $flags{option_cache}->{$opt} = $self->option($opt, 1);
-  }
-  my %opt = %{ $flags{option_cache} };
-
   return 'amaflags != 2'
-    if $opt{'use_amaflags'} && $cdr->amaflags != 2;
-
-  return "disposition NOT IN ( $opt{'disposition_in'} )"
-    if $opt{'disposition_in'} =~ /\S/
-    && !grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $opt{'disposition_in'});
-    
-  return "disposition IN ( $opt{'ignore_disposition'} )"
-    if $opt{'ignore_disposition'} =~ /\S/
-    && grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $opt{'ignore_disposition'});
-
-  return "carrierid != $opt{'use_carrierid'}"
-    if length($opt{'use_carrierid'})
-    && $cdr->carrierid ne $opt{'use_carrierid'}; #ne otherwise 0 matches ''
+    if $self->option_cacheable('use_amaflags') && $cdr->amaflags != 2;
 
-  return "cdrtypenum != $opt{'use_cdrtypenum'}"
-    if length($opt{'use_cdrtypenum'})
-    && $cdr->cdrtypenum ne $opt{'use_cdrtypenum'}; #ne otherwise 0 matches ''
-    
-  return "cdrtypenum == $opt{'ignore_cdrtypenum'}"
-    if length($opt{'ignore_cdrtypenum'})
-    && $cdr->cdrtypenum eq $opt{'ignore_cdrtypenum'}; #eq otherwise 0 matches ''
+  return "disposition NOT IN ( ". $self->option_cacheable('disposition_in')." )"
+    if $self->option_cacheable('disposition_in') =~ /\S/
+    && !grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('disposition_in'));
+  
+  return "disposition IN ( ". $self->option_cacheable('ignore_disposition')." )"
+    if $self->option_cacheable('ignore_disposition') =~ /\S/
+    && grep { $cdr->disposition eq $_ } split(/\s*,\s*/, $self->option_cacheable('ignore_disposition'));
+
+  return "carrierid NOT IN ( ". $self->option_cacheable('use_carrierid'). " )"
+    if $self->option_cacheable('use_carrierid') =~ /\S/
+    && !grep { $cdr->carrierid eq $_ } split(/\s*,\s*/, $self->option_cacheable('use_carrierid')); #eq otherwise 0 matches ''
+
+  # unlike everything else, use_cdrtypenum is applied in FS::svc_x::get_cdrs.
+  return "cdrtypenum != ". $self->option_cacheable('use_cdrtypenum')
+    if length($self->option_cacheable('use_cdrtypenum'))
+    && $cdr->cdrtypenum ne $self->option_cacheable('use_cdrtypenum'); #ne otherwise 0 matches ''
+  
+  return "cdrtypenum == ". $self->option_cacheable('ignore_cdrtypenum')
+    if length($self->option_cacheable('ignore_cdrtypenum'))
+    && $cdr->cdrtypenum eq $self->option_cacheable('ignore_cdrtypenum'); #eq otherwise 0 matches ''
+
+  # unlike everything else, use_calltypenum is applied in FS::svc_x::get_cdrs.
+  return "calltypenum != ". $self->option_cacheable('use_calltypenum')
+    if length($self->option_cacheable('use_calltypenum'))
+    && $cdr->calltypenum ne $self->option_cacheable('use_calltypenum'); #ne otherwise 0 matches ''
+  
+  return "calltypenum == ". $self->option_cacheable('ignore_calltypenum')
+    if length($self->option_cacheable('ignore_calltypenum'))
+    && $cdr->calltypenum eq $self->option_cacheable('ignore_calltypenum'); #eq otherwise 0 matches ''
 
-  return "dcontext IN ( $opt{'skip_dcontext'} )"
-    if $opt{'skip_dcontext'} =~ /\S/
-    && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $opt{'skip_dcontext'});
+  return "dcontext IN ( ". $self->option_cacheable('skip_dcontext'). " )"
+    if $self->option_cacheable('skip_dcontext') =~ /\S/
+    && grep { $cdr->dcontext eq $_ } split(/\s*,\s*/, $self->option_cacheable('skip_dcontext'));
 
-  my $len_prefix = length($opt{'skip_dstchannel_prefix'});
-  return "dstchannel starts with $opt{'skip_dstchannel_prefix'}"
+  my $len_prefix = length($self->option_cacheable('skip_dstchannel_prefix'));
+  return "dstchannel starts with ". $self->option_cacheable('skip_dstchannel_prefix')
     if $len_prefix
-    && substr($cdr->dstchannel,0,$len_prefix) eq $opt{'skip_dstchannel_prefix'};
+    && substr($cdr->dstchannel,0,$len_prefix) eq $self->option_cacheable('skip_dstchannel_prefix');
 
-  my $dst_length = $opt{'skip_dst_length_less'};
+  my $dst_length = $self->option_cacheable('skip_dst_length_less');
   return "destination less than $dst_length digits"
     if $dst_length && length($cdr->dst) < $dst_length;
 
-  return "lastapp is $opt{'skip_lastapp'}"
-    if length($opt{'skip_lastapp'}) && $cdr->lastapp eq $opt{'skip_lastapp'};
+  return "lastapp is ". $self->option_cacheable('skip_lastapp')
+    if length($self->option_cacheable('skip_lastapp')) && $cdr->lastapp eq $self->option_cacheable('skip_lastapp');
 
   #all right then, rate it
   '';
index fb7a8d3..9ce8e6a 100644 (file)
@@ -49,12 +49,13 @@ Destination package (see L<FS::part_pkg>)
 =item link_type
 
 Link type - currently, "bill" (source package bills a line item from target
-package), or "svc" (source package includes services from target package).
+package), or "svc" (source package includes services from target package), 
+or "supp" (ordering source package creates a target package).
 
 =item hidden
 
 Flag indicating that this subpackage should be felt, but not seen as an invoice
-line item when set to 'Y'
+line item when set to 'Y'.  Not allowed for "supp" links.
 
 =back
 
@@ -119,11 +120,26 @@ sub check {
     $self->ut_numbern('pkglinknum')
     || $self->ut_foreign_key('src_pkgpart', 'part_pkg', 'pkgpart')
     || $self->ut_foreign_key('dst_pkgpart', 'part_pkg', 'pkgpart')
-    || $self->ut_enum('link_type', [ 'bill', 'svc' ] )
+    || $self->ut_enum('link_type', [ 'bill', 'svc', 'supp' ] )
     || $self->ut_enum('hidden', [ '', 'Y' ] )
   ;
   return $error if $error;
 
+  if ( $self->link_type eq 'supp' ) {
+    # some sanity checking
+    my $src_pkg = $self->src_pkg;
+    my $dst_pkg = $self->dst_pkg;
+    if ( $src_pkg->freq eq '0' and $dst_pkg->freq ne '0' ) {
+      return "One-time charges can't have supplemental packages."
+    } elsif ( $dst_pkg->freq ne '0' ) {
+      my $ratio = $dst_pkg->freq / $src_pkg->freq;
+      if ($ratio != int($ratio)) {
+        return "Supplemental package period (pkgpart ".$dst_pkg->pkgpart.
+               ") must be an integer multiple of main package period.";
+      }
+    }
+  }
+
   $self->SUPER::check;
 }
 
diff --git a/FS/FS/part_pkg_msgcat.pm b/FS/FS/part_pkg_msgcat.pm
new file mode 100644 (file)
index 0000000..7c00c26
--- /dev/null
@@ -0,0 +1,138 @@
+package FS::part_pkg_msgcat;
+
+use strict;
+use base qw( FS::Record );
+use FS::Locales;
+#use FS::Record qw( qsearch qsearchs );
+use FS::part_pkg;
+
+=head1 NAME
+
+FS::part_pkg_msgcat - Object methods for part_pkg_msgcat records
+
+=head1 SYNOPSIS
+
+  use FS::part_pkg_msgcat;
+
+  $record = new FS::part_pkg_msgcat \%hash;
+  $record = new FS::part_pkg_msgcat { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_msgcat object represents localized labels of a package
+definition.  FS::part_pkg_msgcat inherits from FS::Record.  The following
+fields are currently supported:
+
+=over 4
+
+=item pkgpartmsgnum
+
+primary key
+
+=item pkgpart
+
+Package definition
+
+=item locale
+
+locale
+
+=item pkg
+
+Localized package name (customer-viewable)
+
+=item comment
+
+Localized package comment (non-customer-viewable), optional
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'part_pkg_msgcat'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('pkgpartmsgnum')
+    || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart')
+    || $self->ut_enum('locale', [ FS::Locales->locales ] )
+    || $self->ut_text('pkg')
+    || $self->ut_textn('comment')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg_usage.pm b/FS/FS/part_pkg_usage.pm
new file mode 100644 (file)
index 0000000..99014d3
--- /dev/null
@@ -0,0 +1,159 @@
+package FS::part_pkg_usage;
+
+use strict;
+use base qw( FS::m2m_Common FS::Record );
+use FS::Record qw( qsearch qsearchs );
+use Scalar::Util qw(blessed);
+
+=head1 NAME
+
+FS::part_pkg_usage - Object methods for part_pkg_usage records
+
+=head1 SYNOPSIS
+
+  use FS::part_pkg_usage;
+
+  $record = new FS::part_pkg_usage \%hash;
+  $record = new FS::part_pkg_usage { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_usage object represents a stock of usage minutes (generally
+for voice services) included in a package definition.  FS::part_pkg_usage 
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item pkgusagepart - primary key
+
+=item pkgpart - the package definition (L<FS::part_pkg>)
+
+=item minutes - the number of minutes included per billing cycle
+
+=item priority - the relative order in which to use this stock of minutes.
+
+=item shared - 'Y' to allow these minutes to be shared with other packages
+belonging to the same customer.  Otherwise, only usage allocated to this
+package will use this stock of minutes.
+
+=item rollover - 'Y' to allow unused minutes to carry over between billing
+cycles.  Otherwise, the available minutes will reset to the value of the 
+"minutes" field upon billing.
+
+=item description - a text description of this stock of minutes
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+=item insert CLASSES
+
+=item replace CLASSES
+
+CLASSES can be an array or hash of usage classnums (see L<FS::usage_class>)
+to link to this record.
+
+=item delete
+
+=cut
+
+sub table { 'part_pkg_usage'; }
+
+sub insert {
+  my $self = shift;
+  my $opt = ref($_[0]) eq 'HASH' ? shift : { @_ };
+
+  $self->SUPER::insert
+  || $self->process_m2m( 'link_table'   => 'part_pkg_usage_class',
+                         'target_table' => 'usage_class',
+                         'params'       => $opt,
+  );
+}
+
+sub replace {
+  my $self = shift;
+  my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+              ? shift
+              : $self->replace_old;
+  my $opt = ref($_[0]) eq 'HASH' ? $_[0] : { @_ };
+  $self->SUPER::replace($old)
+  || $self->process_m2m( 'link_table'   => 'part_pkg_usage_class',
+                         'target_table' => 'usage_class',
+                         'params'       => $opt,
+  );
+}
+
+sub delete {
+  my $self = shift;
+  $self->process_m2m( 'link_table'   => 'part_pkg_usage_class',
+                      'target_table' => 'usage_class',
+                      'params'       => {},
+  ) || $self->SUPER::delete;
+}
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('pkgusagepart')
+    || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart')
+    || $self->ut_number('minutes')
+    || $self->ut_numbern('priority')
+    || $self->ut_flag('shared')
+    || $self->ut_flag('rollover')
+    || $self->ut_textn('description')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=item classnums
+
+Returns the usage class numbers that are allowed to use minutes from this
+pool.
+
+=cut
+
+sub classnums {
+  my $self = shift;
+  if (!$self->get('classnums')) {
+    my $classnums = [
+      map { $_->classnum }
+      qsearch('part_pkg_usage_class', { 'pkgusagepart' => $self->pkgusagepart })
+    ];
+    $self->set('classnums', $classnums);
+  }
+  @{ $self->get('classnums') };
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg_usage_class.pm b/FS/FS/part_pkg_usage_class.pm
new file mode 100644 (file)
index 0000000..9a99783
--- /dev/null
@@ -0,0 +1,125 @@
+package FS::part_pkg_usage_class;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::part_pkg_usage_class - Object methods for part_pkg_usage_class records
+
+=head1 SYNOPSIS
+
+  use FS::part_pkg_usage_class;
+
+  $record = new FS::part_pkg_usage_class \%hash;
+  $record = new FS::part_pkg_usage_class { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_usage_class object is a link between a package usage stock
+(L<FS::part_pkg_usage>) and a voice usage class (L<FS::usage_class)>.
+FS::part_pkg_usage_class inherits from FS::Record.  The following fields 
+are currently supported:
+
+=over 4
+
+=item num - primary key
+
+=item pkgusagepart - L<FS::part_pkg_usage> key
+
+=item classnum - L<FS::usage_class> key.  Set to null to allow this stock
+to be used for calls that have no usage class.  To avoid confusion, you
+should only do this if you don't use usage classes on your system.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example.  To add the example to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'part_pkg_usage_class'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('num')
+    || $self->ut_foreign_key('pkgusagepart', 'part_pkg_usage', 'pkgusagepart')
+    || $self->ut_foreign_keyn('classnum', 'usage_class', 'classnum')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+The author forgot to customize this manpage.
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index c471771..da794dd 100644 (file)
@@ -58,6 +58,13 @@ L<FS::svc_domain>, and L<FS::svc_forward>, among others.
 
 =item preserve - Preserve after cancellation, empty or 'Y'
 
+=item selfservice_access - Access allowed to the service via self-service:
+empty for full access, "readonly" for read-only, "hidden" to hide it entirely
+
+=item restrict_edit_password - Require the "Provision customer service" access
+right to change the password field, rather than just "Edit password".  Only
+relevant to svc_acct for now.
+
 =back
 
 =head1 METHODS
@@ -391,7 +398,8 @@ sub check {
     || $self->ut_enum('preserve', [ '', 'Y' ] )
     || $self->ut_enum('selfservice_access', [ '', 'hidden', 'readonly' ] )
     || $self->ut_foreign_keyn('classnum', 'part_svc_class', 'classnum' )
-  ;
+    || $self->ut_enum('restrict_edit_password', [ '', 'Y' ] )
+;
   return $error if $error;
 
   my @fields = eval { fields( $self->svcdb ) }; #might die
@@ -749,11 +757,9 @@ sub process {
                     if ( $flag =~ /^[MAH]$/ ) {
                       $param->{ $f } = delete( $param->{ $f.'_classnum' } );
                     }
-                   if ( $flag =~ /^S$/ 
-                          or $_ eq 'usergroup' ) {
-                      $param->{ $f } = ref($param->{ $f })
-                                         ? join(',', @{$param->{ $f }} )
-                                         : $param->{ $f };
+                   if ( ( $flag =~ /^[MAHS]$/ or $_ eq 'usergroup' )
+                         and ref($param->{ $f }) ) {
+                      $param->{ $f } = join(',', @{ $param->{ $f } });
                    }
                     ( $f, $f.'_flag', $f.'_label' );
                   }
index d467516..38ce1fa 100644 (file)
@@ -99,8 +99,14 @@ sub check {
   $self->columnflag(uc($1));
 
   if ( $self->columnflag =~ /^[MA]$/ ) {
-    $error =
-      $self->ut_foreign_key( 'columnvalue', 'inventory_class', 'classnum' );
+    # split, check all values independently, and normalize
+    my @classnums = split(/\s*,\s*/, $self->columnvalue);
+    foreach (@classnums) {
+      $self->set('columnvalue', $_);
+      $error = $self->ut_foreign_key( 'columnvalue', 'inventory_class', 'classnum' );
+      return $error if $error;
+    }
+    $self->set('columnvalue', join(',', @classnums));
   }
   if ( $self->columnflag eq 'H' ) {
     $error = 
index b8da9b4..2a048a1 100644 (file)
@@ -201,7 +201,7 @@ foreach my $INC (@INC) {
              \\%FS::pay_batch::$mod\::export_info,
              \$FS::pay_batch::$mod\::name)";
     $name ||= $mod; # in case it's not defined
-    if$@) {
+    if ($@) {
       # in FS::cdr this is a die, not a warn.  That's probably a bug.
       warn "error using FS::pay_batch::$mod (skipping): $@\n";
       next;
@@ -401,12 +401,12 @@ sub import_results {
       foreach ('paid', '_date', 'payinfo') {
         $new_cust_pay_batch->$_($hash{$_}) if $hash{$_};
       }
-      $error = $new_cust_pay_batch->approve($hash{'paybatch'} || $self->batchnum);
+      $error = $new_cust_pay_batch->approve(%hash);
       $total += $hash{'paid'};
 
     } elsif ( &{$declined_condition}(\%hash) ) {
 
-      $error = $new_cust_pay_batch->decline;
+      $error = $new_cust_pay_batch->decline($hash{'error_message'});;
 
     }
 
@@ -572,8 +572,6 @@ sub import_from_gateway {
       my $payby; # CARD or CHEK
       my $error;
 
-      # follow realtime gateway practice here
-      # though eventually this stuff should go into separate fields...
       my $paybatch = $gateway->gatewaynum .  '-' .  $gateway->gateway_module .
         ':' . $item->authorization .  ':' . $item->order_number;
 
@@ -644,8 +642,11 @@ sub import_from_gateway {
             payby       => $payby,
             invnum      => $item->invoice_number,
             batchnum    => $pay_batch->batchnum,
-            paybatch    => $paybatch,
             payinfo     => $payinfo,
+            gatewaynum  => $gateway->gatewaynum,
+            processor   => $gateway->gateway_module,
+            auth        => $item->authorization,
+            order_number => $item->order_number,
           }
         );
         $error ||= $cust_pay->insert;
@@ -725,7 +726,12 @@ sub import_from_gateway {
         # approval status
         if ( $item->approved ) {
           # follow Billing_Realtime format for paybatch
-          $error = $cust_pay_batch->approve($paybatch);
+          $error = $cust_pay_batch->approve(
+            'gatewaynum'    => $gateway->gatewaynum,
+            'processor'     => $gateway->gateway_module,
+            'auth'          => $item->authorization,
+            'order_number'  => $item->order_number,
+          );
           $total += $cust_pay_batch->paid;
         }
         else {
@@ -829,6 +835,9 @@ sub try_to_resolve {
       }
       return $error if $error;
     }
+  } elsif ( @unresolved ) {
+    # auto resolve is not enabled, and we're not ready to resolve
+    return;
   }
 
   $self->set_status('R');
@@ -1028,7 +1037,6 @@ sub manual_approve {
   my $self = shift;
   my $date = time;
   my %opt = @_;
-  my $paybatch = $opt{'paybatch'} || $self->batchnum;
   my $usernum = $opt{'usernum'} || die "manual approval requires a usernum";
   my $conf = FS::Conf->new;
   return 'manual batch approval disabled' 
@@ -1058,7 +1066,9 @@ sub manual_approve {
       '_date'   => $date,
       'usernum' => $usernum,
     };
-    my $error = $new_cust_pay_batch->approve($paybatch);
+    my $error = $new_cust_pay_batch->approve();
+    # there are no approval options here (authorization, order_number, etc.)
+    # because the transaction wasn't really approved
     if ( $error ) {
       $dbh->rollback;
       return 'paybatchnum '.$cust_pay_batch->paybatchnum.": $error";
index 719b504..a3708d4 100644 (file)
@@ -31,13 +31,13 @@ $name = 'BoM';
   },
   header => sub { 
     my $pay_batch = shift;
-    sprintf( "A%10s%04u%06u%05u%54s\n",  #80
+    sprintf( "A%10s%04u%06u%05u%53s\n",  #80
       $origid,
       $pay_batch->batchnum,
       jdate($pay_batch->download),
       $datacenter,
       "") .
-    sprintf( "XD%03u%06u%-15s%-30s%09u%-12s   \n", #80
+    sprintf( "XD%03u%06u%-15s%-30s%09u%-12s ", #80
       $typecode,
       jdate($pay_batch->download),
       $shortname,
@@ -48,7 +48,7 @@ $name = 'BoM';
   row => sub {
     my ($cust_pay_batch, $pay_batch) = @_;
     my ($account, $aba) = split('@', $cust_pay_batch->payinfo);
-    sprintf( "D%010.0f%09u%-12s%-29s%-19s\n", #80
+    sprintf( "D%010.0f%09u%-12s%-29s%-18s ", #80
       $cust_pay_batch->amount * 100,
       $aba,
       $account,
@@ -58,8 +58,8 @@ $name = 'BoM';
   },
   footer => sub {
     my ($pay_batch, $batchcount, $batchtotal) = @_;
-    sprintf( "YD%08u%014.0f%56s\n", $batchcount, $batchtotal*100, ""). #80
-    sprintf( "Z%014u%04u%014u%05u%42s\n",  #80 now
+    sprintf( "YD%08u%014.0f%55s\n", $batchcount, $batchtotal*100, ""). #80
+    sprintf( "Z%014u%05u%014u%05u%40s",  #80 now
       $batchtotal*100, $batchcount, "0", "0", "");
   },
 );
index 220fecb..b24c9c3 100644 (file)
@@ -25,12 +25,6 @@ my %holiday_yearly = (
   12 => { map {$_=>1} 26 }, #boxing day
 );
 my %holiday = (
-  2012 => {
-             7 => { map {$_=>1}  2 }, #canada day
-             8 => { map {$_=>1}  6 }, #First Monday of August Civic Holiday
-             9 => { map {$_=>1}  3 }, #labour day
-            10 => { map {$_=>1}  8 }, #thanksgiving
-          },
   2013 => {  2 => { map {$_=>1} 18 }, #family day
              3 => { map {$_=>1} 29 }, #good friday
              4 => { map {$_=>1}  1 }, #easter monday
diff --git a/FS/FS/pay_batch/nacha.pm b/FS/FS/pay_batch/nacha.pm
new file mode 100644 (file)
index 0000000..c069082
--- /dev/null
@@ -0,0 +1,208 @@
+package FS::pay_batch::nacha;
+
+use strict;
+use vars qw( %import_info %export_info $name $conf $entry_hash $DEBUG );
+use Date::Format;
+#use Time::Local 'timelocal';
+#use FS::Conf;
+
+$name = 'NACHA';
+
+$DEBUG = 0;
+
+%import_info = (
+  #XXX stub finish me
+  'filetype' => 'CSV',
+  'fields' => [
+  ],
+  'hook' => sub {
+    my $hash = shift;
+  },
+  'approved' => sub { 1 },
+  'declined' => sub { 0 },
+);
+
+%export_info = (
+
+  #optional
+  init => sub {
+    $conf = shift;
+  },
+
+  delimiter => '',
+
+
+  header => sub {
+    my( $pay_batch, $cust_pay_batch_arrayref ) = @_;
+
+    $conf->config('batchconfig-nacha-destination') =~ /^\s*(\d{9})\s*$/
+      or die 'illegal NACHA Destination';
+    my $dest = $1;
+
+    my $dest_name = $conf->config('batchconfig-nacha-destination_name');
+    $dest_name = substr( $dest_name. (' 'x23), 0, 23);
+
+    $conf->config('batchconfig-nacha-origin') =~ /^\s*(\d{10})\s*$/
+      or die 'illegal NACHA Origin';
+    my $origin = $1;
+
+    my $company = $conf->config('company_name', $pay_batch->agentnum);
+    $company = substr(uc($company). (' 'x23), 0, 23);
+
+    my $now = time;
+
+    #haha don't want to break after a quarter million years of a batch a day
+    #or 54 years for 5000 agent-virtualized hosted companies batching daily
+    my $refcode = substr( (' 'x8). $pay_batch->batchnum, -8);
+
+    #or only 25,000 years or 5.4 for 5000 companies :)
+    #though they would probably want them numbered per company
+    my $batchnum = substr( ('0'x7). $pay_batch->batchnum, -7);
+
+    $entry_hash = 0;
+
+    warn "building File & Batch Header Records\n" if $DEBUG;
+
+    ##
+    # File Header Record
+    ##
+
+    '1'.                      #Record Type Code
+    '01'.                     #Priority Code
+    ' '. $dest.               #Immediate Destination / 9-digit transit routing #
+    $origin.                  #Immediate Origin / 10 digit company number
+    time2str('%y%m%d', $now). #File Creation Date
+    time2str('%H%M',   $now). #File Creation Time
+    'A'.                 #XXX file ID modifier, mult. files in transit? [A-Z0-9]
+    '094'.                    #94 character records
+    '10'.                     #Blocking Factor
+    '1'.                      #Format code
+    $dest_name.               #Immediate Destination Name / 23 char bank name
+    $company.                 #Immediate Origin Name / 23 char company name
+    $refcode.                 #Reference Code (internal/optional)
+
+    ###
+    # Batch Header Record
+    ###
+
+    '5'.                     #Record Type Code
+    '225'.                   #Service Class Code (220 credits only,
+                             #                    200 mixed debits & credits)
+    substr($company, 0, 16). #on cust. statements
+    (' 'x20 ).               #20 char "company internal use if desired"
+    $origin.                 #Company Identification (Immediate Origin)
+    'PPD'. #others?
+           #PPD "Prearranged Payments and Deposit entries" for consumer items
+           #CCD (Cash Concentration and Disbursement)
+           #CTX (Corporate Trade Exchange)
+           #TEL (Telephone initiated entires)
+           #WEB (Authorization received via the Internet)
+    'InterntSvc'. #XXX from conf 10 char txn desc, printed on cust. statements
+
+    #6 char "Descriptive date" printed on customer statements
+    #XXX now? or use a separate post date?
+    time2str('%y%m%d', $now).
+
+    #6 char date transactions are to be posted
+    #XXX now? or do we need a future banking day date like eft_canada trainwreck
+    time2str('%y%m%d', $now).
+
+    (' 'x3).                 #Settlement Date / Reserved
+    '1'.                     #Originator Status Code
+    substr($dest, 0, 8).     #Originating Financial Institution
+    $batchnum                #Batch Number ("number batches sequentially")
+
+  },
+
+  'row' => sub {
+    my( $cust_pay_batch, $pay_batch, $batchcount, $batchtotal ) = @_;
+
+    my ($account, $aba) = split('@', $cust_pay_batch->payinfo);
+
+    # "Total of all positions 4-11 on each 6 record"
+    $entry_hash += substr($aba,0,8); 
+
+    my $cust_main = $cust_pay_batch->cust_main;
+    my $cust_identifier = substr($cust_main->display_custnum. (' 'x15), 0, 15);
+
+    #XXX paytype should actually be in the batch, but this will do for now
+    #27 checking debit, 37 savings debit
+    my $transaction_code = ( $cust_main->paytype =~ /savings/i ? '37' : '27' );
+
+    my $cust_name = substr($cust_main->name. (' 'x22), 0, 22);
+
+    #non-PPD transactions?  future
+
+    warn "building PPD Record\n" if $DEBUG;
+
+    ###
+    # PPD Entry Detail Record
+    ###
+
+    '6'.                              #Record Type Code
+    $transaction_code.                #Transaction Code
+    $aba.                             #Receiving DFI Identification, check digit
+    substr($account.(' 'x17), 0, 17). #DFI Account number (Left justify)
+    sprintf('%010d', $cust_pay_batch->amount * 100). #Amount
+    $cust_identifier.                 #Individual Identification Number, 15 char
+    $cust_name.                       #Individual name (22-char)
+    '  '.                             #2 char "company internal use if desired"
+    '0'.                              #Addenda Record Indicator
+    (' 'x15)                          #15 digit "bank will assign trace number"
+                                      # (00000?)
+  },
+
+  'footer' => sub {
+    my( $pay_batch, $batchcount, $batchtotal ) = @_;
+
+    #Only use the final 10 positions in the entry
+    $entry_hash = substr( '00'.$entry_hash, -10); 
+
+    $conf->config('batchconfig-nacha-destination') =~ /^\s*(\d{9})\s*$/
+      or die 'illegal NACHA Destination';
+    my $dest = $1;
+
+    $conf->config('batchconfig-nacha-origin') =~ /^\s*(\d{10})\s*$/
+      or die 'illegal NACHA Origin';
+    my $origin = $1;
+
+    my $batchnum = substr( ('0'x7). $pay_batch->batchnum, -7);
+
+    warn "building Batch & File Control Records\n" if $DEBUG;
+
+    ###
+    # Batch Control Record
+    ###
+
+    '8'.                          #Record Type Code
+    '225'.                        #Service Class Code (220 credits only,
+                                  #                    200 mixed debits&credits)
+    sprintf('%06d', $batchcount). #Entry / Addenda Count
+    $entry_hash.
+    sprintf('%012d', $batchtotal * 100). #Debit total
+    '000000000000'.               #Credit total
+    $origin.                      #Company Identification (Immediate Origin)
+    (' 'x19).                     #Message Authentication Code (19 char blank)
+    (' 'x6).                      #Federal Reserve Use (6 char blank)
+    substr($dest, 0, 8).          #Originating Financial Institution
+    $batchnum.                    #Batch Number ("number batches sequentially")
+
+    ###
+    # File Control Record
+    ###
+
+    '9'.                                 #Record Type Code
+    '000001'.                            #Batch Counter (# of batch header recs)
+    sprintf('%06d', $batchcount + 4).    #num of physical blocks on the file..?
+    sprintf('%08d', $batchcount).        #total # of entry detail and addenda
+    $entry_hash.
+    sprintf('%012d', $batchtotal * 100). #Debit total
+    '000000000000'.                      #Credit total
+    ( ' 'x39 )                           #Reserved / blank
+
+  },
+
+);
+
+1;
+
index c687cc8..1ecf35a 100644 (file)
@@ -23,7 +23,10 @@ my $gateway;
     '_date',
     'approvalStatus',
     'order_number',
-    'authorization',
+    'auth',
+    'procStatus',
+    'procStatusMessage',
+    'respCodeMessage',
     ],
   xmlkeys     => [
     'orderID',
@@ -31,6 +34,9 @@ my $gateway;
     'approvalStatus',
     'txRefNum',
     'authorizationCode',
+    'procStatus',
+    'procStatusMessage',
+    'respCodeMessage',
     ],
   'hook'        => sub {
       if ( !$gateway ) {
@@ -38,7 +44,7 @@ my $gateway;
         # as the batch config, if there is one.  If not, leave 
         # gateway out entirely.
         my $merchant = (FS::Conf->new->config('batchconfig-paymentech'))[2];
-        my $g = qsearchs({
+        $gateway = qsearchs({
               'table'     => 'payment_gateway',
               'addl_from' => ' JOIN payment_gateway_option USING (gatewaynum) ',
               'hashref'   => {  disabled    => '',
@@ -46,18 +52,19 @@ my $gateway;
                                 optionvalue => $merchant,
                               },
               });
-        $gateway = ($g ? $g->gatewaynum . '-' : '') . 'PaymenTech';
       }
       my ($hash, $oldhash) = @_;
+      $hash->{'gatewaynum'} = $gateway->gatewaynum if $gateway;
+      $hash->{'processor'} = 'PaymenTech';
       my ($mon, $day, $year, $hour, $min, $sec) = 
         $hash->{'_date'} =~ /^(..)(..)(....)(..)(..)(..)$/;
       $hash->{'_date'} = timelocal($sec, $min, $hour, $day, $mon-1, $year);
       $hash->{'paid'} = $oldhash->{'amount'};
-      $hash->{'paybatch'} = join(':', 
-        $gateway,
-        $hash->{'authorization'},
-        $hash->{'order_number'},
-      );
+      if ( $hash->{'procStatus'} == 0 ) {
+        $hash->{'error_message'} = $hash->{'respCodeMessage'};
+      } else {
+        $hash->{'error_message'} = $hash->{'procStatusMessage'};
+      }
     },
   'approved'    => sub { my $hash = shift;
                             $hash->{'approvalStatus'} 
@@ -103,32 +110,32 @@ my %paymentech_countries = map { $_ => 1 } qw( US CA GB UK );
       $xml->startTag('newOrder', BatchRequestNo => $count++);
       my $status = $_->cust_main->status;
       tie my %order, 'Tie::IxHash', (
-        industryType => 'EC',
-        transType    => 'AC',
-        bin          => $bin,
-        merchantID   => $merchantID,
-        terminalID   => $terminalID,
+        industryType    => 'EC',
+        transType       => 'AC',
+        bin             => $bin,
+        merchantID      => $merchantID,
+        terminalID      => $terminalID,
         ($_->payby eq 'CARD') ? (
-          ccAccountNum => $_->payinfo,
-          ccExp        => $_->expmmyy,
+          ccAccountNum    => $_->payinfo,
+          ccExp           => $_->expmmyy,
         ) : (
           ecpCheckRT      => ($_->payinfo =~ /@(\d+)/),
           ecpCheckDDA     => ($_->payinfo =~ /(\d+)@/),
           ecpBankAcctType => $paytype{lc($_->cust_main->paytype)},
           ecpDelvMethod   => 'A',
         ),
-        avsZip          => substr($_->zip, 0, 10),
+        avsZip          => substr($_->zip,      0, 10),
         avsAddress1     => substr($_->address1, 0, 30),
         avsAddress2     => substr($_->address2, 0, 30),
-        avsCity         => substr($_->city, 0, 20),
-        avsState        => $_->state,
-        avsName        => substr($_->first . ' ' . $_->last, 0, 30),
-        avsCountryCode => ( $paymentech_countries{ $_->country }
-                              ? $_->country
-                              : ''
-                          ),
-        orderID        => $_->paybatchnum,
-        amount         => $_->amount * 100,
+        avsCity         => substr($_->city,     0, 20),
+        avsState        => substr($_->state,    0, 2),
+        avsName         => substr($_->first. ' '. $_->last, 0, 30),
+        ( $paymentech_countries{ $_->country }
+          ? ( avsCountryCode  => $_->country )
+          : ()
+        ),
+        orderID           => $_->paybatchnum,
+        amount            => $_->amount * 100,
         );
       # only do this if recurringInd is enabled in config, 
       # and the customer has at least one non-canceled recurring package
index 093891e..50659ac 100644 (file)
@@ -73,10 +73,7 @@ sub _parse_paybatch {
     my $payment_gateway =
       qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
 
-    die "payment gateway $gatewaynum not found" #?
-      unless $payment_gateway;
-
-    $processor = $payment_gateway->gateway_module;
+    $processor = $payment_gateway->gateway_module if $payment_gateway;
 
   }
 
index b5d51d3..a18c8ff 100644 (file)
@@ -2,7 +2,7 @@ package FS::prospect_main;
 
 use strict;
 use base qw( FS::Quotable_Mixin FS::o2m_Common FS::Record );
-use vars qw( $DEBUG );
+use vars qw( $DEBUG @location_fields );
 use Scalar::Util qw( blessed );
 use FS::Record qw( dbh qsearch qsearchs );
 use FS::agent;
@@ -12,6 +12,43 @@ use FS::qual;
 
 $DEBUG = 0;
 
+#started as false laziness w/cust_main/Location.pm
+
+use Carp qw(carp);
+
+my $init = 0;
+BEGIN {
+  # set up accessors for location fields
+  if (!$init) {
+    no strict 'refs';
+    @location_fields = 
+      qw( address1 address2 city county state zip country district
+        latitude longitude coord_auto censustract censusyear geocode
+        addr_clean );
+
+    foreach my $f (@location_fields) {
+      *{"FS::prospect_main::$f"} = sub {
+        carp "WARNING: tried to set cust_main.$f with accessor" if (@_ > 1);
+        my @cust_location = shift->cust_location or return '';
+        #arbitrarily picking the first because the UI only lets you add one
+        $cust_location[0]->$f
+      };
+    }
+    $init++;
+  }
+}
+
+#debugging shim--probably a performance hit, so remove this at some point
+sub get {
+  my $self = shift;
+  my $field = shift;
+  if ( $DEBUG and grep { $_ eq $field } @location_fields ) {
+    carp "WARNING: tried to get() location field $field";
+    $self->$field;
+  }
+  $self->FS::Record::get($field);
+}
+
 =head1 NAME
 
 FS::prospect_main - Object methods for prospect_main records
@@ -208,6 +245,12 @@ sub check {
   ;
   return $error if $error;
 
+  my $company = $self->company;
+  $company =~ s/^\s+//; 
+  $company =~ s/\s+$//; 
+  $company =~ s/\s+/ /g;
+  $self->company($company);
+
   $self->SUPER::check;
 }
 
index bf2711b..47f13e6 100644 (file)
@@ -176,6 +176,36 @@ sub _total {
 
 }
 
+#prevent things from falsely showing up as taxes, at least until we support
+# quoting tax amounts..
+sub _items_tax {
+  return ();
+}
+sub _items_nontax {
+  shift->cust_bill_pkg;
+}
+
+sub _items_total {
+  my( $self, $total_items ) = @_;
+
+  if ( $self->total_setup > 0 ) {
+    push @$total_items, {
+      'total_item'   => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ),
+      'total_amount' => $self->total_setup,
+    };
+  }
+
+  #could/should add up the different recurring frequencies on lines of their own
+  # but this will cover the 95% cases for now
+  if ( $self->total_recur > 0 ) {
+    push @$total_items, {
+      'total_item'   => $self->mt('Total Recurring'),
+      'total_amount' => $self->total_recur,
+    };
+  }
+
+}
+
 =item enable_previous
 
 =cut
index 3d40bb0..efff968 100644 (file)
@@ -1,10 +1,12 @@
 package FS::quotation_pkg;
 
 use strict;
-use base qw( FS::Record );
+use base qw( FS::TemplateItem_Mixin FS::Record );
 use FS::Record qw( qsearchs ); #qsearch
 use FS::part_pkg;
 use FS::cust_location;
+use FS::quotation;
+use FS::quotation_pkg_discount; #so its loaded when TemplateItem_Mixin needs it
 
 =head1 NAME
 
@@ -80,6 +82,14 @@ points to.  You can ask the object for a copy with the I<hash> method.
 
 sub table { 'quotation_pkg'; }
 
+sub display_table         { 'quotation_pkg'; }
+
+#forget it, just overriding cust_bill_pkg_display entirely
+#sub display_table_orderby { 'quotationpkgnum'; } # something else?
+#                                                 #  (for invoice display order)
+
+sub discount_table        { 'quotation_pkg_discount'; }
+
 =item insert
 
 Adds this record to the database.  If there is an error, returns the error,
@@ -107,8 +117,9 @@ sub check {
 
   my $error = 
     $self->ut_numbern('quotationpkgnum')
-    || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart' )
-    || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum' )
+    || $self->ut_foreign_key(  'quotationnum', 'quotation',    'quotationnum' )
+    || $self->ut_foreign_key(  'pkgpart',      'part_pkg',     'pkgpart'      )
+    || $self->ut_foreign_keyn( 'locationnum', 'cust_location', 'locationnum'  )
     || $self->ut_numbern('start_date')
     || $self->ut_numbern('contract_end')
     || $self->ut_numbern('quantity')
@@ -131,7 +142,7 @@ sub desc {
 
 sub setup {
   my $self = shift;
-  return '0.00' if $self->waive_setup eq 'Y';
+  return '0.00' if $self->waive_setup eq 'Y' || $self->{'_NO_SETUP_KLUDGE'};
   my $part_pkg = $self->part_pkg;
   #my $setup = $part_pkg->can('base_setup') ? $part_pkg->base_setup
   #                                         : $part_pkg->option('setup_fee');
@@ -144,6 +155,7 @@ sub setup {
 
 sub recur {
   my $self = shift;
+  return '0.00' if $self->{'_NO_RECUR_KLUDGE'};
   my $part_pkg = $self->part_pkg;
   my $recur = $part_pkg->can('base_recur') ? $part_pkg->base_recur
                                            : $part_pkg->option('recur_fee');
@@ -152,6 +164,43 @@ sub recur {
   sprintf('%.2f', $recur);
 }
 
+=item cust_bill_pkg_display [ type => TYPE ]
+
+=cut
+
+sub cust_bill_pkg_display {
+  my ( $self, %opt ) = @_;
+
+  my $type = $opt{type} if exists $opt{type};
+  return () if $type eq 'U'; #quotations don't have usage
+
+  if ( $self->get('display') ) {
+    return ( grep { defined($type) ? ($type eq $_->type) : 1 }
+               @{ $self->get('display') }
+           );
+  } else {
+
+    #??
+    my $setup = $self->new($self->hashref);
+    $setup->{'_NO_RECUR_KLUDGE'} = 1;
+    $setup->{'type'} = 'S';
+    my $recur = $self->new($self->hashref);
+    $recur->{'_NO_SETUP_KLUDGE'} = 1;
+    $recur->{'type'} = 'R';
+
+    if ( $type eq 'S' ) {
+      return ($setup);
+    } elsif ( $type eq 'R' ) {
+      return ($recur);
+    } else {
+      #return ($setup, $recur);
+      return ($self);
+    }
+
+  }
+
+}
+
 =back
 
 =head1 BUGS
index a2511cf..49ac938 100644 (file)
@@ -308,17 +308,28 @@ sub dest_detail {
     #find a rate prefix, first look at most specific, then fewer digits,
     # finally trying the country code only
     my $rate_prefix = '';
-    for my $len ( reverse(1..10) ) {
-      $rate_prefix = qsearchs('rate_prefix', {
+    $rate_prefix = qsearchs({
+        'table'     => 'rate_prefix',
+        'addl_from' => ' JOIN rate_region USING (regionnum)',
+        'hashref'   => {
+          'countrycode' => $countrycode,
+          'npa'         => $phonenum,
+        },
+        'extra_sql' => ' AND exact_match = \'Y\''
+    });
+    if (!$rate_prefix) {
+      for my $len ( reverse(1..10) ) {
+        $rate_prefix = qsearchs('rate_prefix', {
+          'countrycode' => $countrycode,
+          #'npa'         => { op=> 'LIKE', value=> substr($number, 0, $len) }
+          'npa'         => substr($phonenum, 0, $len),
+        } ) and last;
+      }
+      $rate_prefix ||= qsearchs('rate_prefix', {
         'countrycode' => $countrycode,
-        #'npa'         => { op=> 'LIKE', value=> substr($number, 0, $len) }
-        'npa'         => substr($phonenum, 0, $len),
-      } ) and last;
+        'npa'         => '',
+      });
     }
-    $rate_prefix ||= qsearchs('rate_prefix', {
-      'countrycode' => $countrycode,
-      'npa'         => '',
-    });
 
     return '' unless $rate_prefix;
 
index f4a0ab1..d42fdb4 100644 (file)
@@ -36,7 +36,10 @@ inherits from FS::Record.  The following fields are currently supported:
 
 =item regionnum - primary key
 
-=item regionname
+=item regionname - name of the region
+
+=item exact_match - 'Y' if "prefixes" in this region really represent 
+complete phone numbers.  Null if they represent prefixes (the usual case).
 
 =back
 
@@ -233,6 +236,7 @@ sub check {
   my $error =
        $self->ut_numbern('regionnum')
     || $self->ut_text('regionname')
+    || $self->ut_flag('exact_match')
   ;
   return $error if $error;
 
index 7aede54..0aea455 100644 (file)
@@ -43,27 +43,6 @@ inherit from, i.e. FS::svc_acct.  FS::svc_Common inherits from FS::Record.
 
 =over 4
 
-=item search_sql_field FIELD STRING
-
-Class method which returns an SQL fragment to search for STRING in FIELD.
-
-It is now case-insensitive by default.
-
-=cut
-
-sub search_sql_field {
-  my( $class, $field, $string ) = @_;
-  my $table = $class->table;
-  my $q_string = dbh->quote($string);
-  "LOWER($table.$field) = LOWER($q_string)";
-}
-
-#fallback for services that don't provide a search... 
-sub search_sql {
-  #my( $class, $string ) = @_;
-  '1 = 0'; #false
-}
-
 =item new
 
 =cut
@@ -863,13 +842,20 @@ sub set_auto_inventory {
     next if $columnflag eq 'A' && $self->$field() ne '';
 
     my $classnum = $part_svc_column->columnvalue;
-    my %hash = ( 'classnum' => $classnum );
+    my %hash;
 
     if ( $columnflag eq 'A' && $self->$field() eq '' ) {
       $hash{'svcnum'} = '';
     } elsif ( $columnflag eq 'M' ) {
       return "Select inventory item for $field" unless $self->getfield($field);
       $hash{'item'} = $self->getfield($field);
+      my $chosen_classnum = $self->getfield($field.'_classnum');
+      if ( grep {$_ == $chosen_classnum} split(',', $classnum) ) {
+        $classnum = $chosen_classnum;
+      }
+      # otherwise the chosen classnum is either (all), or somehow not on 
+      # the list, so ignore it and choose the first item that's in any
+      # class on the list
     }
 
     my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql(
@@ -880,18 +866,30 @@ sub set_auto_inventory {
     my $inventory_item = qsearchs({
       'table'     => 'inventory_item',
       'hashref'   => \%hash,
-      'extra_sql' => "AND $agentnums_sql",
+      'extra_sql' => "AND classnum IN ($classnum) AND $agentnums_sql",
       'order_by'  => 'ORDER BY ( agentnum IS NULL ) '. #agent inventory first
                      ' LIMIT 1 FOR UPDATE',
     });
 
     unless ( $inventory_item ) {
+      # should really only be shown if columnflag eq 'A'...
       $dbh->rollback if $oldAutoCommit;
-      my $inventory_class =
-        qsearchs('inventory_class', { 'classnum' => $classnum } );
-      return "Can't find inventory_class.classnum $classnum"
-        unless $inventory_class;
-      return "Out of ". PL_N($inventory_class->classname);
+      my $message = 'Out of ';
+      my @classnums = split(',', $classnum);
+      foreach ( @classnums ) {
+        my $class = FS::inventory_class->by_key($_)
+          or return "Can't find inventory_class.classnum $_";
+        $message .= PL_N($class->classname);
+        if ( scalar(@classnums) > 2 ) { # english is hard
+          if ( $_ != $classnums[-1] ) {
+            $message .= ', ';
+          }
+        }
+        if ( scalar(@classnums) > 1 and $_ == $classnums[-2] ) {
+          $message .= 'and ';
+        }
+      }
+      return $message;
     }
 
     next if $columnflag eq 'M' && $inventory_item->svcnum == $self->svcnum;
@@ -899,13 +897,14 @@ sub set_auto_inventory {
     $self->setfield( $field, $inventory_item->item );
       #if $columnflag eq 'A' && $self->$field() eq '';
 
+    # release the old inventory item, if there was one
     if ( $old && $old->$field() && $old->$field() ne $self->$field() ) {
       my $old_inv = qsearchs({
         'table'     => 'inventory_item',
-        'hashref'   => { 'classnum' => $classnum,
+        'hashref'   => { 
                          'svcnum'   => $old->svcnum,
                        },
-        'extra_sql' => ' AND '.
+        'extra_sql' => "AND classnum IN ($classnum) AND ".
           '( ( svc_field IS NOT NULL AND svc_field = '.$dbh->quote($field).' )'.
           '  OR ( svc_field IS NULL AND item = '. dbh->quote($old->$field).' )'.
           ')',
@@ -941,6 +940,9 @@ sub set_auto_inventory {
 
 =item return_inventory
 
+Release all inventory items attached to this service's fields.  Call
+when unprovisioning the service.
+
 =cut
 
 sub return_inventory {
@@ -1082,17 +1084,22 @@ otherwise returns false.
 
 =cut
 
-sub export_setstatus {
-  my( $self, @args ) = @_;
-  my $error = $self->export('setstatus', @args);
+sub export_setstatus { shift->_export_setstatus_X('setstatus', @_) }
+sub export_setstatus_listadd { shift->_export_setstatus_X('setstatus_listadd', @_) }
+sub export_setstatus_listdel { shift->_export_setstatus_X('setstatus_listdel', @_) }
+sub export_setstatus_vacationadd { shift->_export_setstatus_X('setstatus_vacationadd', @_) }
+sub export_setstatus_vacationdel { shift->_export_setstatus_X('setstatus_vacationdel', @_) }
+
+sub _export_setstatus_X {
+  my( $self, $method, @args ) = @_;
+  my $error = $self->export($method, @args);
   if ( $error ) {
-    warn "error running export_setstatus: $error";
+    warn "error running export_$method: $error";
     return $error;
   }
   '';
 }
 
-
 =item export HOOK [ EXPORT_ARGS ]
 
 Runs the provided export hook (i.e. "suspend", "unsuspend") for this service.
@@ -1277,6 +1284,221 @@ sub nms_ip_delete {
 #XXX not yet implemented
 }
 
+=item search_sql_field FIELD STRING
+
+Class method which returns an SQL fragment to search for STRING in FIELD.
+
+It is now case-insensitive by default.
+
+=cut
+
+sub search_sql_field {
+  my( $class, $field, $string ) = @_;
+  my $table = $class->table;
+  my $q_string = dbh->quote($string);
+  "LOWER($table.$field) = LOWER($q_string)";
+}
+
+#fallback for services that don't provide a search... 
+sub search_sql {
+  #my( $class, $string ) = @_;
+  '1 = 0'; #false
+}
+
+=item search HASHREF
+
+Class method which returns a qsearch hash expression to search for parameters
+specified in HASHREF.
+
+Parameters:
+
+=over 4
+
+=item unlinked - set to search for all unlinked services.  Overrides all other options.
+
+=item agentnum
+
+=item custnum
+
+=item svcpart
+
+=item ip_addr
+
+=item pkgpart - arrayref
+
+=item routernum - arrayref
+
+=item sectornum - arrayref
+
+=item towernum - arrayref
+
+=item order_by
+
+=back
+
+=cut
+
+# svc_broadband::search should eventually use this instead
+sub search {
+  my ($class, $params) = @_;
+
+  my @from = (
+    'LEFT JOIN cust_svc  USING ( svcnum  )',
+    'LEFT JOIN part_svc  USING ( svcpart )',
+    'LEFT JOIN cust_pkg  USING ( pkgnum  )',
+    FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg'),
+  );
+
+  my @where = ();
+
+  $class->_search_svc($params, \@from, \@where) if $class->can('_search_svc');
+
+#  # domain
+#  if ( $params->{'domain'} ) { 
+#    my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
+#    #preserve previous behavior & bubble up an error if $svc_domain not found?
+#    push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
+#  }
+#
+#  # domsvc
+#  if ( $params->{'domsvc'} =~ /^(\d+)$/ ) { 
+#    push @where, "domsvc = $1";
+#  }
+
+  #unlinked
+  push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
+
+  #agentnum
+  if ( $params->{'agentnum'} =~ /^(\d+)$/ && $1 ) {
+    push @where, "cust_main.agentnum = $1";
+  }
+
+  #custnum
+  if ( $params->{'custnum'} =~ /^(\d+)$/ && $1 ) {
+    push @where, "custnum = $1";
+  }
+
+  #customer status
+  if ( $params->{'cust_status'} =~ /^([a-z]+)$/ ) {
+    push @where, FS::cust_main->cust_status_sql . " = '$1'";
+  }
+
+  #customer balance
+  if ( $params->{'balance'} =~ /^\s*(\-?\d*(\.\d{1,2})?)\s*$/ && length($1) ) {
+    my $balance = $1;
+
+    my $age = '';
+    if ( $params->{'balance_days'} =~ /^\s*(\d*(\.\d{1,3})?)\s*$/ && length($1) ) {
+      $age = time - 86400 * $1;
+    }
+    push @where, FS::cust_main->balance_date_sql($age) . " > $balance";
+  }
+
+  #payby
+  if ( $params->{'payby'} && scalar(@{ $params->{'payby'} }) ) {
+    my @payby = map "'$_'", grep /^(\w+)$/, @{ $params->{'payby'} };
+    push @where, 'payby IN ('. join(',', @payby ). ')';
+  }
+
+  #pkgpart
+  ##pkgpart, now properly untainted, can be arrayref
+  #for my $pkgpart ( $params->{'pkgpart'} ) {
+  #  if ( ref $pkgpart ) {
+  #    my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
+  #    push @where, "cust_pkg.pkgpart IN ($where)" if $where;
+  #  }
+  #  elsif ( $pkgpart =~ /^(\d+)$/ ) {
+  #    push @where, "cust_pkg.pkgpart = $1";
+  #  }
+  #}
+  if ( $params->{'pkgpart'} ) {
+    my @pkgpart = ref( $params->{'pkgpart'} )
+                    ? @{ $params->{'pkgpart'} }
+                    : $params->{'pkgpart'}
+                      ? ( $params->{'pkgpart'} )
+                      : ();
+    @pkgpart = grep /^(\d+)$/, @pkgpart;
+    push @where, 'cust_pkg.pkgpart IN ('. join(',', @pkgpart ). ')' if @pkgpart;
+  }
+
+  #svcnum
+  if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
+    push @where, "svcnum = $1";
+  }
+
+  # svcpart
+  if ( $params->{'svcpart'} ) {
+    my @svcpart = ref( $params->{'svcpart'} )
+                    ? @{ $params->{'svcpart'} }
+                    : $params->{'svcpart'}
+                      ? ( $params->{'svcpart'} )
+                      : ();
+    @svcpart = grep /^(\d+)$/, @svcpart;
+    push @where, 'svcpart IN ('. join(',', @svcpart ). ')' if @svcpart;
+  }
+
+  if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
+    push @from, ' LEFT JOIN export_svc USING ( svcpart )';
+    push @where, "exportnum = $1";
+  }
+
+#  # sector and tower
+#  my @where_sector = $class->tower_sector_sql($params);
+#  if ( @where_sector ) {
+#    push @where, @where_sector;
+#    push @from, ' LEFT JOIN tower_sector USING ( sectornum )';
+#  }
+
+  # here is the agent virtualization
+  #if ($params->{CurrentUser}) {
+  #  my $access_user =
+  #    qsearchs('access_user', { username => $params->{CurrentUser} });
+  #
+  #  if ($access_user) {
+  #    push @where, $access_user->agentnums_sql('table'=>'cust_main');
+  #  }else{
+  #    push @where, "1=0";
+  #  }
+  #} else {
+    push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
+                   'table'      => 'cust_main',
+                   'null_right' => 'View/link unlinked services',
+                 );
+  #}
+
+  push @where, @{ $params->{'where'} } if $params->{'where'};
+
+  my $addl_from = join(' ', @from);
+  my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
+
+  my $table = $class->table;
+
+  my $count_query = "SELECT COUNT(*) FROM $table $addl_from $extra_sql";
+  #if ( keys %svc_X ) {
+  #  $count_query .= ' WHERE '.
+  #                    join(' AND ', map "$_ = ". dbh->quote($svc_X{$_}),
+  #                                      keys %svc_X
+  #                        );
+  #}
+
+  {
+    'table'       => $table,
+    'hashref'     => {},
+    'select'      => join(', ',
+                       "$table.*",
+                       'part_svc.svc',
+                       'cust_main.custnum',
+                       @{ $params->{'addl_select'} || [] },
+                       FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
+                     ),
+    'addl_from'   => $addl_from,
+    'extra_sql'   => $extra_sql,
+    'order_by'    => $params->{'order_by'},
+    'count_query' => $count_query,
+  };
+
+}
+
 =back
 
 =head1 BUGS
index 6adbc6f..3da07c1 100644 (file)
@@ -27,12 +27,10 @@ towernum or sectornum can also contain 'none' to allow null values.
 =cut
 
 sub tower_sector_sql {
-  my $class = shift;
-  my $params = shift;
-  return '' unless keys %$params;
-  my $where = '';
+  my( $class, $params ) = @_;
+  return () unless keys %$params;
 
-  my @where;
+  my @where = ();
   for my $field (qw(towernum sectornum)) {
     my $value = $params->{$field} or next;
     if ( ref $value and grep { $_ } @$value ) {
index 8e71d82..26d6e5b 100644 (file)
@@ -15,6 +15,7 @@ use vars qw( $DEBUG $me $conf $skip_fuzzyfiles
              $username_noperiod $username_nounderscore $username_nodash
              $username_uppercase $username_percent $username_colon
              $username_slash $username_equals $username_pound
+             $username_exclamation
              $password_noampersand $password_noexclamation
              $warning_template $warning_from $warning_subject $warning_mimetype
              $warning_cc
@@ -85,6 +86,7 @@ FS::UID->install_callback( sub {
   $username_slash = $conf->exists('username-slash');
   $username_equals = $conf->exists('username-equals');
   $username_pound = $conf->exists('username-pound');
+  $username_exclamation = $conf->exists('username-exclamation');
   $password_noampersand = $conf->exists('password-noexclamation');
   $password_noexclamation = $conf->exists('password-noexclamation');
   $dirhash = $conf->config('dirhash') || 0;
@@ -1193,7 +1195,7 @@ sub check {
 
   my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
 
-  $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:\/\=\#]{$usernamemin,$ulen})$/i
+  $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:\/\=\#\!]{$usernamemin,$ulen})$/i
     or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
   $recref->{username} = $1;
 
@@ -1234,6 +1236,9 @@ sub check {
   unless ( $username_pound ) {
     $recref->{username} =~ /\#/ and return $uerror;
   }
+  unless ( $username_exclamation ) {
+    $recref->{username} =~ /\!/ and return $uerror;
+  }
 
 
   $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
@@ -1890,12 +1895,14 @@ sub email {
   $self->username. '@'. $self->domain(@_);
 }
 
+
 =item acct_snarf
 
 Returns an array of FS::acct_snarf records associated with the account.
 
 =cut
 
+# unused as originally intended, but now by Communigate Pro "RPOP"
 sub acct_snarf {
   my $self = shift;
   qsearch({
@@ -2817,116 +2824,39 @@ Arrayref of additional WHERE clauses, will be ANDed together.
 
 =cut
 
-sub search {
-  my ($class, $params) = @_;
-
-  my @from = (
-    ' LEFT JOIN cust_svc  USING ( svcnum  ) ',
-    ' LEFT JOIN part_svc  USING ( svcpart ) ',
-    ' LEFT JOIN cust_pkg  USING ( pkgnum  ) ',
-    ' LEFT JOIN cust_main USING ( custnum ) ',
-  );
+sub _search_svc {
+  my( $class, $params, $from, $where ) = @_;
 
-  my @where = ();
+  #these two should probably move to svc_Domain_Mixin ?
 
   # domain
   if ( $params->{'domain'} ) { 
     my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
     #preserve previous behavior & bubble up an error if $svc_domain not found?
-    push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
+    push @$where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
   }
 
   # domsvc
   if ( $params->{'domsvc'} =~ /^(\d+)$/ ) { 
-    push @where, "domsvc = $1";
+    push @$where, "domsvc = $1";
   }
 
-  #unlinked
-  push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
-
-  #agentnum
-  if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
-    push @where, "cust_main.agentnum = $1";
-  }
-
-  #custnum
-  if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
-    push @where, "custnum = $1";
-  }
-
-  #pkgpart
-  if ( $params->{'pkgpart'} && scalar(@{ $params->{'pkgpart'} }) ) {
-    #XXX untaint or sql quote
-    push @where,
-      'cust_pkg.pkgpart IN ('. join(',', @{ $params->{'pkgpart'} } ). ')';
-  }
 
   # popnum
   if ( $params->{'popnum'} =~ /^(\d+)$/ ) { 
-    push @where, "popnum = $1";
+    push @$where, "popnum = $1";
   }
 
-  # svcpart
-  if ( $params->{'svcpart'} =~ /^(\d+)$/ ) { 
-    push @where, "svcpart = $1";
-  }
 
-  if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
-    push @from, ' LEFT JOIN export_svc USING ( svcpart )';
-    push @where, "exportnum = $1";
-  }
+  #and these in svc_Tower_Mixin, or maybe we never should have done svc_acct
+  # towers (or, as mark thought, never should have done svc_broadband)
 
   # sector and tower
   my @where_sector = $class->tower_sector_sql($params);
   if ( @where_sector ) {
-    push @where, @where_sector;
-    push @from, ' LEFT JOIN tower_sector USING ( sectornum )';
-  }
-
-  # here is the agent virtualization
-  #if ($params->{CurrentUser}) {
-  #  my $access_user =
-  #    qsearchs('access_user', { username => $params->{CurrentUser} });
-  #
-  #  if ($access_user) {
-  #    push @where, $access_user->agentnums_sql('table'=>'cust_main');
-  #  }else{
-  #    push @where, "1=0";
-  #  }
-  #} else {
-    push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
-                   'table'      => 'cust_main',
-                   'null_right' => 'View/link unlinked services',
-                 );
-  #}
-
-  push @where, @{ $params->{'where'} } if $params->{'where'};
-
-  my $addl_from = join(' ', @from);
-  my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
-
-  my $count_query = "SELECT COUNT(*) FROM svc_acct $addl_from $extra_sql";
-  #if ( keys %svc_acct ) {
-  #  $count_query .= ' WHERE '.
-  #                    join(' AND ', map "$_ = ". dbh->quote($svc_acct{$_}),
-  #                                      keys %svc_acct
-  #                        );
-  #}
-
-  my $sql_query = {
-    'table'       => 'svc_acct',
-    'hashref'     => {}, # \%svc_acct,
-    'select'      => join(', ',
-                       'svc_acct.*',
-                       'part_svc.svc',
-                       'cust_main.custnum',
-                       FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
-                     ),
-    'addl_from'   => $addl_from,
-    'extra_sql'   => $extra_sql,
-    'order_by'    => $params->{'order_by'},
-    'count_query' => $count_query,
-  };
+    push @$where, @where_sector;
+    push @$from, ' LEFT JOIN tower_sector USING ( sectornum )';
+  }
 
 }
 
index af81353..002aa55 100755 (executable)
@@ -103,10 +103,10 @@ sub table_info {
     'ip_field' => 'ip_addr',
     'fields' => {
       'svcnum'      => 'Service',
-      'description' => 'Descriptive label for this particular device',
-      'speed_down'  => 'Maximum download speed for this service in Kbps.  0 denotes unlimited.',
-      'speed_up'    => 'Maximum upload speed for this service in Kbps.  0 denotes unlimited.',
-      'ip_addr'     => 'IP address.  Leave blank for automatic assignment.',
+      'description' => 'Descriptive label',
+      'speed_down'  => 'Download speed (Kbps)',
+      'speed_up'    => 'Upload speed (Kbps)',
+      'ip_addr'     => 'IP address',
       'blocknum'    => 
       { 'label' => 'Address block',
                          'type'  => 'select',
@@ -134,6 +134,15 @@ sub table_info {
                          disable_inventory => 1,
                          multiple => 1,
                        },
+      'radio_serialnum' => 'Radio Serial Number',
+      'radio_location'  => 'Radio Location',
+      'poe_location'    => 'POE Location',
+      'rssi'            => 'RSSI',
+      'suid'            => 'SUID',
+      'shared_svcnum'   => { label             => 'Shared Service',
+                             type              => 'search-svc_broadband',
+                             disable_inventory => 1,
+                           },
     },
   };
 }
@@ -175,115 +184,44 @@ Parameters:
 
 =cut
 
-sub search {
-  my ($class, $params) = @_;
-  my @where = ();
-  my @from = (
-    'LEFT JOIN cust_svc  USING ( svcnum  )',
-    'LEFT JOIN part_svc  USING ( svcpart )',
-    'LEFT JOIN cust_pkg  USING ( pkgnum  )',
-    'LEFT JOIN cust_main USING ( custnum )',
-  );
-
-  # based on FS::svc_acct::search, probably the most mature of the bunch
-  #unlinked
-  push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
-  
-  #agentnum
-  if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
-    push @where, "cust_main.agentnum = $1";
-  }
-  push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
-    'null_right' => 'View/link unlinked services',
-    'table' => 'cust_main'
-  );
-
-  #custnum
-  if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
-    push @where, "custnum = $1";
-  }
-
-  #pkgpart, now properly untainted, can be arrayref
-  for my $pkgpart ( $params->{'pkgpart'} ) {
-    if ( ref $pkgpart ) {
-      my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
-      push @where, "cust_pkg.pkgpart IN ($where)" if $where;
-    }
-    elsif ( $pkgpart =~ /^(\d+)$/ ) {
-      push @where, "cust_pkg.pkgpart = $1";
-    }
-  }
+sub _search_svc {
+  my( $class, $params, $from, $where ) = @_;
 
   #routernum, can be arrayref
   for my $routernum ( $params->{'routernum'} ) {
     # this no longer uses addr_block
     if ( ref $routernum and grep { $_ } @$routernum ) {
       my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
-      my @orwhere;
+      my @orwhere = ();
       push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
       push @orwhere, "svc_broadband.routernum IS NULL" 
         if grep /^none$/, @$routernum;
-      push @where, '( '.join(' OR ', @orwhere).' )';
+      push @$where, '( '.join(' OR ', @orwhere).' )';
     }
     elsif ( $routernum =~ /^(\d+)$/ ) {
-      push @where, "svc_broadband.routernum = $1";
+      push @$where, "svc_broadband.routernum = $1";
     }
     elsif ( $routernum eq 'none' ) {
-      push @where, "svc_broadband.routernum IS NULL";
+      push @$where, "svc_broadband.routernum IS NULL";
     }
   }
 
+  #this should probably move to svc_Tower_Mixin, or maybe we never should have
+  # done svc_acct # towers (or, as mark thought, never should have done
+  # svc_broadband)
+
   #sector and tower, as above
   my @where_sector = $class->tower_sector_sql($params);
   if ( @where_sector ) {
-    push @where, @where_sector;
-    push @from, 'LEFT JOIN tower_sector USING ( sectornum )';
+    push @$where, @where_sector;
+    push @$from, 'LEFT JOIN tower_sector USING ( sectornum )';
   }
  
-  #svcnum
-  if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
-    push @where, "svcnum = $1";
-  }
-
-  #svcpart
-  if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
-    push @where, "svcpart = $1";
-  }
-
-  #exportnum
-  if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
-    push @from, 'LEFT JOIN export_svc USING ( svcpart )';
-    push @where, "exportnum = $1";
-  }
-
   #ip_addr
   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
-    push @where, "ip_addr = '$1'";
+    push @$where, "ip_addr = '$1'";
   }
 
-  #custnum
-  if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
-    push @where, "custnum = $1";
-  }
-  
-  my $addl_from = join(' ', @from);
-  my $extra_sql = '';
-  $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
-  my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
-  return( {
-      'table'   => 'svc_broadband',
-      'hashref' => {},
-      'select'  => join(', ',
-        'svc_broadband.*',
-        'part_svc.svc',
-        'cust_main.custnum',
-        FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
-      ),
-      'extra_sql' => $extra_sql,
-      'addl_from' => $addl_from,
-      'order_by'  => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
-      'count_query' => $count_query,
-    } );
 }
 
 =item search_sql STRING
@@ -296,15 +234,31 @@ sub search_sql {
   my( $class, $string ) = @_;
   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
     $class->search_sql_field('ip_addr', $string );
-  }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
+  } elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
     $class->search_sql_field('mac_addr', uc($string));
-  }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
+  } elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
+  } elsif ( $string =~ /^(\d+)$/ ) {
+    my $table = $class->table;
+    "$table.svcnum = $1";
   } else {
     '1 = 0'; #false
   }
 }
 
+=item smart_search STRING
+
+=cut
+
+sub smart_search {
+  my( $class, $string ) = @_;
+  qsearch({
+    'table'     => $class->table, #'svc_broadband',
+    'hashref'   => {},
+    'extra_sql' => 'WHERE '. $class->search_sql($string),
+  });
+}
+
 =item label
 
 Returns the IP address.
@@ -313,7 +267,12 @@ Returns the IP address.
 
 sub label {
   my $self = shift;
-  $self->ip_addr;
+  my $label = 'IP:'. ($self->ip_addr || 'Unknown');
+  $label .= ', MAC:'. $self->mac_addr
+    if $self->mac_addr;
+  $label .= ' ('. $self->description. ')'
+    if $self->description;
+  return $label;
 }
 
 =item insert [ , OPTION => VALUE ... ]
@@ -377,7 +336,7 @@ sub check {
 
   # remove delimiters
   my $mac_addr = uc($self->get('mac_addr'));
-  $mac_addr =~ s/[-: ]//g;
+  $mac_addr =~ s/[\W_]//g;
   $self->set('mac_addr', $mac_addr);
 
   my $error =
@@ -396,6 +355,12 @@ sub check {
     || $self->ut_sfloatn('altitude')
     || $self->ut_textn('vlan_profile')
     || $self->ut_textn('plan_id')
+    || $self->ut_alphan('radio_serialnum')
+    || $self->ut_textn('radio_location')
+    || $self->ut_textn('poe_location')
+    || $self->ut_snumbern('rssi')
+    || $self->ut_numbern('suid')
+    || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
   ;
   return $error if $error;
 
index 10f7b68..7ca20cc 100644 (file)
@@ -40,6 +40,10 @@ fields are currently supported:
 
 primary key
 
+=item exportnum
+
+Export definition, see L<FS::part_export>
+
 =item svcnum
 
 Customer service, see L<FS::cust_svc>
index af6865f..b28cc9e 100644 (file)
@@ -105,9 +105,13 @@ sub search_sql {
   my ($class, $string) = @_;
   my @where = ();
 
-  my $ip = NetAddr::IP->new($string);
-  if ( $ip ) {
-    push @where, $class->search_sql_field('ip_addr', $ip->addr);
+  if ( $string =~ /^[\d\.:]+$/ ) {
+    # if the string isn't an IP address, this will waste several seconds
+    # attempting a DNS lookup.  so try to filter those out.
+    my $ip = NetAddr::IP->new($string);
+    if ( $ip ) {
+      push @where, $class->search_sql_field('ip_addr', $ip->addr);
+    }
   }
   
   if ( $string =~ /^(\w+)$/ ) {
@@ -164,7 +168,7 @@ sub check {
   return $x unless ref $x;
 
   my $hw_addr = $self->getfield('hw_addr');
-  $hw_addr = join('', split(/\W/, $hw_addr));
+  $hw_addr = join('', split(/[_\W]/, $hw_addr));
   if ( $conf->exists('svc_hardware-check_mac_addr') ) {
     $hw_addr = uc($hw_addr);
     $hw_addr =~ /^[0-9A-F]{12}$/ 
index 4182a13..66e51da 100644 (file)
@@ -292,7 +292,9 @@ to allow title to indicate a range of IP addresses.
 
 =item begin, end: Start and end of date range, as unix timestamp.
 
-=item cdrtypenum: Only return CDRs with this type number.
+=item cdrtypenum: Only return CDRs with this type.
+
+=item calltypenum: Only return CDRs with this call type.
 
 =back
 
@@ -310,6 +312,9 @@ sub psearch_cdrs {
   if ($options{'cdrtypenum'}) {
     $hash{'cdrtypenum'} = $options{'cdrtypenum'};
   }
+  if ($options{'calltypenum'}) {
+    $hash{'calltypenum'} = $options{'calltypenum'};
+  }
 
   my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
 
index 1296c1e..3cc1adc 100644 (file)
@@ -23,10 +23,11 @@ $DEBUG = 0;
 @pw_set = ( 'a'..'k', 'm','n', 'p-z', 'A'..'N', 'P'..'Z' , '2'..'9' );
 
 #ask FS::UID to run this stuff for us later
-$FS::UID::callback{'FS::svc_acct'} = sub { 
+FS::UID->install_callback( sub { 
   $conf = new FS::Conf;
   $phone_name_max = $conf->config('svc_phone-phone_name-max_length');
-};
+}
+);
 
 =head1 NAME
 
@@ -68,6 +69,10 @@ primary key
 
 =item phonenum
 
+=item sim_imsi
+
+SIM IMSI (http://en.wikipedia.org/wiki/International_mobile_subscriber_identity)
+
 =item sip_password
 
 =item pin
@@ -147,6 +152,7 @@ sub table_info {
                             disable_select => 1,
                           },
         'phonenum'     => 'Phone number',
+        'sim_imsi'     => 'IMSI', #http://en.wikipedia.org/wiki/International_mobile_subscriber_identity
         'pin'          => { label => 'Voicemail PIN', #'Personal Identification Number',
                             type  => 'text',
                             disable_inventory => 1,
@@ -466,6 +472,7 @@ sub check {
     $self->ut_numbern('svcnum')
     || $self->ut_numbern('countrycode')
     || $self->$phonenum_check_method('phonenum')
+    || $self->ut_numbern('sim_imsi')
     || $self->ut_anything('sip_password')
     || $self->ut_numbern('pin')
     || $self->ut_textn('phone_name')
@@ -486,6 +493,10 @@ sub check {
   ;
   return $error if $error;
 
+  return 'Illegal IMSI (not 14-15 digits)' #shorter?
+    if length($self->sim_imsi)
+    && ( length($self->sim_imsi) < 14 || length($self->sim_imsi) > 15 );
+
     # LNP data validation
     return 'Cannot set LNP fields: no LNP in progress'
        if ( ($self->lnp_desired_due_date || $self->lnp_due_date 
@@ -673,10 +684,14 @@ with the chosen prefix.
 
 =item begin, end: Start and end of a date range, as unix timestamp.
 
-=item cdrtypenum: Only return CDRs with this type number.
+=item cdrtypenum: Only return CDRs with this type.
+
+=item calltypenum: Only return CDRs with this call type.
 
 =item disable_src => 1: Only match on "charged_party", not "src".
 
+=item nonzero: Only return CDRs where duration > 0.
+
 =item by_svcnum: not supported for svc_phone
 
 =item billsec_sum: Instead of returning all of the CDRs, return a single
@@ -722,6 +737,9 @@ sub psearch_cdrs {
   if ($options{'cdrtypenum'}) {
     $hash{'cdrtypenum'} = $options{'cdrtypenum'};
   }
+  if ($options{'calltypenum'}) {
+    $hash{'calltypenum'} = $options{'calltypenum'};
+  }
   
   my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
 
@@ -744,6 +762,9 @@ sub psearch_cdrs {
   if ( $options{'end'} ) {
     push @where, 'startdate < '.  $options{'end'};
   }
+  if ( $options{'nonzero'} ) {
+    push @where, 'duration > 0';
+  }
 
   my $extra_sql = ( keys(%hash) ? ' AND ' : ' WHERE ' ). join(' AND ', @where );
 
@@ -770,6 +791,30 @@ sub get_cdrs {
   qsearch ( $psearch->{query} )
 }
 
+=item sum_cdrs
+
+Takes the same options as psearch_cdrs, but returns a single row containing
+"count" (the number of CDRs) and the sums of the following fields: duration,
+billsec, rated_price, rated_seconds, rated_minutes.
+
+Note that if any calls are not rated, their rated_* fields will be null.
+If you want to use those fields, pass the 'status' option to limit to 
+calls that have been rated.  This is intentional; please don't "fix" it.
+
+=cut
+
+sub sum_cdrs {
+  my $self = shift;
+  my $psearch = $self->psearch_cdrs(@_);
+  $psearch->{query}->{'select'} = join(',',
+    'COUNT(*) AS count',
+    map { "SUM($_) AS $_" }
+      qw(duration billsec rated_price rated_seconds rated_minutes)
+  );
+  # hack
+  $psearch->{query}->{'extra_sql'} =~ s/ ORDER BY.*$//;
+  qsearchs ( $psearch->{query} );
+}
 
 =back
 
index f954fe8..9423290 100644 (file)
@@ -13,6 +13,7 @@ bin/freeside-deloutsource
 bin/freeside-deloutsourceuser
 bin/freeside-deluser
 bin/freeside-email
+bin/freeside-phonenum_list
 bin/freeside-queued
 bin/freeside-radgroup
 bin/freeside-reexport
@@ -33,7 +34,6 @@ FS/ClientAPI_SessionCache.pm
 FS/ClientAPI_XMLRPC.pm
 FS/ClientAPI/passwd.pm
 FS/ClientAPI/Agent.pm
-FS/ClientAPI/Bulk.pm
 FS/ClientAPI/MasonComponent.pm
 FS/ClientAPI/MyAccount.pm
 FS/ClientAPI/PrepaidPhone.pm
@@ -74,7 +74,6 @@ FS/cust_main/Billing_Realtime.pm
 FS/cust_main/Import.pm
 FS/cust_main/Packages.pm
 FS/cust_main/Search.pm
-FS/cust_main/_Marketgear.pm
 FS/cust_main_Mixin.pm
 FS/cust_main_county.pm
 FS/cust_main_invoice.pm
@@ -149,8 +148,6 @@ FS/part_pkg/sqlradacct_hour.pm
 FS/part_pkg/subscription.pm
 FS/part_pkg/voip_sqlradacct.pm
 FS/part_pkg/voip_cdr.pm
-FS/part_pkg/base_rate.pm
-FS/part_pkg/base_delayed.pm
 FS/part_pop_local.pm
 FS/part_referral.pm
 FS/part_svc.pm
@@ -493,6 +490,8 @@ FS/phone_type.pm
 t/phone_type.t
 FS/contact_email.pm
 t/contact_email.t
+FS/contact_Mixin.pm
+t/contact_Mixin.t
 FS/prospect_main.pm
 t/prospect_main.t
 FS/o2m_Common.pm
@@ -679,3 +678,15 @@ FS/log.pm
 t/log.t
 FS/log_context.pm
 t/log_context.t
+FS/part_pkg_usage_class.pm
+t/part_pkg_usage_class.t
+FS/cust_pkg_usage.pm
+t/cust_pkg_usage.t
+FS/part_pkg_usage_class.pm
+t/part_pkg_usage_class.t
+FS/part_pkg_usage.pm
+t/part_pkg_usage.t
+FS/cdr_cust_pkg_usage.pm
+t/cdr_cust_pkg_usage.t
+FS/part_pkg_msgcat.pm
+t/part_pkg_msgcat.t
index 7f2693f..c37ff11 100755 (executable)
@@ -218,6 +218,8 @@ or FTP and then import them into the database.
 
 -c: cdrtypenum to set, defaults to none
 
+-g: File is gzipped
+
 user: freeside username
 
 format: CDR format name
index 131b56a..99ea675 100644 (file)
@@ -33,9 +33,11 @@ if ( @cdrtypenums ) {
   $extra_sql .= ' AND cdrtypenum IN ('. join(',', @cdrtypenums ). ')';
 }
 
-our %svcnum = ();
-our %pkgpart = ();
-our %part_pkg = ();
+our %svcnum = ();   # phonenum => svcnum
+our %pkgnum = ();   # phonenum => pkgnum
+our %cust_pkg = (); # pkgnum   => cust_pkg (NOT phonenum => cust_pkg!)
+our %pkgpart = ();  # phonenum => pkgpart
+our %part_pkg = (); # phonenum => part_pkg
 
 #some false laziness w/freeside-cdrrewrited
 
@@ -91,6 +93,9 @@ while (1) {
         next;
       }
 
+      $pkgnum{$number} = $cust_pkg->pkgnum;
+      $cust_pkg{$cust_pkg->pkgnum} ||= $cust_pkg;
+
       #get the package, search through the part_pkg and linked for a voip_cdr def w/matching cdrtypenum (or no use_cdrtypenum)
       my @part_pkg =
         grep { $_->plan eq 'voip_cdr'
@@ -126,10 +131,11 @@ while (1) {
     #}
 
     #XXX if $part_pkg->option('min_included') then we can't prerate this CDR
-      
+    
     my $error = $cdr->rate(
       'part_pkg' => $part_pkg{ $pkgpart{$number} },
-      'svcnum'   => $svcnum{ $number },
+      'cust_pkg' => $cust_pkg{ $pkgnum{$number} },
+      'svcnum'   => $svcnum{$number},
     );
     if ( $error ) {
       #XXX ???
index f2c3926..16f931f 100644 (file)
@@ -30,9 +30,9 @@ die "not running; cdr-asterisk_forward_rewrite, cdr-charged_party_rewrite ".
 
 #--
 
-my %accountcode_unmatch = ();
-my $accountcode_retry = 4 * 60 * 60; # 4 hours
-my $accountcode_giveup = 4 * 24 * 60 * 60; # 4 days
+my %sessionnum_unmatch = ();
+my $sessionnum_retry = 4 * 60 * 60; # 4 hours
+my $sessionnum_giveup = 4 * 24 * 60 * 60; # 4 days
 
 my %cdr_type = map { lc($_->cdrtypename) => $_->cdrtypenum } 
   qsearch('cdr_type',{});
@@ -45,8 +45,8 @@ while (1) {
   # instead of just doing this search like normal CDRs
 
   #hmm :/
-  my @recent = grep { ($accountcode_unmatch{$_} + $accountcode_retry) > time }
-                 keys %accountcode_unmatch;
+  my @recent = grep { ($sessionnum_unmatch{$_} + $sessionnum_retry) > time }
+                 keys %sessionnum_unmatch;
   my $extra_sql = scalar(@recent)
                     ? ' AND acctid NOT IN ('. join(',', @recent). ') '
                     : '';
@@ -136,45 +136,62 @@ while (1) {
 
     }
 
-    if ( $conf->exists('cdr-taqua-accountcode_rewrite')
-         && $cdr->lastapp eq 'acctcode' && $cdr->cdrtypenum == 1
+    if (     $cdr->cdrtypenum == 1
+         and $cdr->lastapp
+         and (
+            $conf->exists('cdr-taqua-accountcode_rewrite') or
+            $conf->exists('cdr-taqua-callerid_rewrite') )
        )
     {
 
       #find the matching CDR
-      my $primary = qsearchs('cdr', {
-        'sessionnum'  => $cdr->sessionnum,
-        'src'         => $cdr->subscriber,
-        #'accountcode' => '',
-      });
+      my %search = ( 'sessionnum' => $cdr->sessionnum );
+      if ( $cdr->lastapp eq 'acctcode' ) {
+        $search{'src'} = $cdr->subscriber;
+      } elsif ( $cdr->lastapp eq 'CallerId' ) {
+        $search{'dst'} = $cdr->subscriber;
+      }
+      my $primary = qsearchs('cdr', \%search);
 
       unless ( $primary ) {
 
         my $cantfind = "can't find primary CDR with session ". $cdr->sessionnum.
                        ", src ". $cdr->subscriber;
-        if ( $cdr->calldate_unix + $accountcode_giveup < time ) {
+        if ( $cdr->calldate_unix + $sessionnum_giveup < time ) {
           warn "ERROR: $cantfind; giving up\n";
-          push @status, 'taqua-accountcode-NOTFOUND';
+          push @status, 'taqua-sessionnum-NOTFOUND';
           $cdr->status('done'); #so it doesn't try to rate
-          delete $accountcode_unmatch{$cdr->acctid}; #so it doesn't suck mem
+          delete $sessionnum_unmatch{$cdr->acctid}; #so it doesn't suck mem
         } else {
           warn "WARNING: $cantfind; will keep trying\n";
-          $accountcode_unmatch{$cdr->acctid} = time;
+          $sessionnum_unmatch{$cdr->acctid} = time;
           next;
         }
 
       } else {
 
-        $primary->accountcode( $cdr->lastdata );
+        if ( $cdr->lastapp eq 'acctcode' ) {
+          # lastdata contains the dialed account code
+          $primary->accountcode( $cdr->lastdata );
+          push @status, 'taqua-accountcode';
+        } elsif ( $cdr->lastapp eq 'CallerId' ) {
+          # lastdata contains "allowed" or "restricted"
+          # or case variants thereof
+          if ( lc($cdr->lastdata) eq 'restricted' ) {
+            $primary->clid( 'PRIVATE' );
+          }
+          push @status, 'taqua-callerid';
+        } else {
+          warn "unknown Taqua service name: ".$cdr->lastapp."\n";
+        }
         #$primary->freesiderewritestatus( 'taqua-accountcode-primary' );
-        my $error = $primary->replace;
+        my $error = $primary->replace if $primary->modified;
         if ( $error ) {
           warn "WARNING: error rewriting primary CDR (will retry): $error\n";
           next;
         }
         $skip{$primary->acctid} = 1;
 
-        push @status, 'taqua-accountcode';
         $cdr->status('done'); #so it doesn't try to rate
 
       }
@@ -214,7 +231,10 @@ sub _shouldrun {
      $conf->exists('cdr-asterisk_forward_rewrite')
   || $conf->exists('cdr-asterisk_australia_rewrite')
   || $conf->exists('cdr-charged_party_rewrite')
-  || $conf->exists('cdr-taqua-accountcode_rewrite');
+  || $conf->exists('cdr-taqua-accountcode_rewrite')
+  || $conf->exists('cdr-taqua-callerid_rewrite')
+  || 0
+  ;
 }
 
 sub usage { 
index 837cc33..9df4db0 100644 (file)
@@ -9,6 +9,7 @@ use FS::UID qw(adminsuidsetup);
 use FS::Record qw(qsearch qsearchs);
 use FS::cust_main;
 use FS::Conf;
+use File::Copy qw(copy);
 use Text::CSV;
 
 my %opt;
@@ -116,7 +117,7 @@ die "failed to connect to '$sftpuser\@$host'\n(".$sftp->error.")\n"
 
 $sftp->setcwd($path) if $path;
 
-my $files = $sftp->ls('.', wanted => qr/\.csv$/, names_only => 1);
+my $files = $sftp->ls('ready', wanted => qr/\.csv$/, names_only => 1);
 if (!@$files) {
   print STDERR "No charge files found.\n" if $opt{v};
   exit(-1);
@@ -129,23 +130,23 @@ my %is_e911 = map {$_ => 1} @E911_CODES;
 
 FILE: foreach my $filename (@$files) {
   print STDERR "Retrieving $filename\n" if $opt{v};
-  $sftp->get("$filename", "$tmpdir/$filename");
+  $sftp->get("ready/$filename", "$tmpdir/$filename");
   if($sftp->error) {
     warn "failed to download $filename\n";
     next FILE;
   }
 
   # make sure server archive dir exists
-  if ( !$sftp->stat('Archive') ) {
-    print STDERR "Creating $path/Archive\n" if $opt{v};
-    $sftp->mkdir('Archive');
+  if ( !$sftp->stat('done') ) {
+    print STDERR "Creating $path/done\n" if $opt{v};
+    $sftp->mkdir('done');
     if($sftp->error) {
       # something is seriously wrong
       die "failed to create archive directory on server:\n".$sftp->error."\n";
     }
   }
   #move to server archive dir
-  $sftp->rename("$filename", "Archive/$filename");
+  $sftp->rename("ready/$filename", "done/$filename");
   if($sftp->error) {
     warn "failed to archive $filename on server:\n".$sftp->error."\n";
   } # process it anyway, I guess/
@@ -159,11 +160,6 @@ FILE: foreach my $filename (@$files) {
   }
 
   open my $fh, "<$tmpdir/$filename";
-  my $header = <$fh>;
-  if ($header !~ /^"cust_id"/) {
-    warn "warning: $filename has incorrect header row:\n$header\n";
-    # but try anyway
-  }
   my $csv = Text::CSV->new; # orthodox CSV
   my %hash;
   while (my $line = <$fh>) {
@@ -172,6 +168,11 @@ FILE: foreach my $filename (@$files) {
       next FILE;
     };
     @hash{@fields} = $csv->fields();
+    if ( $hash{custnum} =~ /^cust/ ) {
+      # there appears to be a header row
+      print STDERR "skipping header row\n" if $opt{v};
+      next;
+    }
     my $cust_main = 
       $cust_main{$hash{custnum}} ||= FS::cust_main->by_key($hash{custnum});
     if (!$cust_main) {
@@ -184,10 +185,11 @@ FILE: foreach my $filename (@$files) {
     my $amount = sprintf('%.2f',$hash{quantity} * $hash{unit_price});
     # construct arguments for $cust_main->charge
     my %charge_opt = (
-      amount      => $amount,
+      amount      => $hash{unit_price},
       quantity    => $hash{quantity},
       start_date  => $cust_main->next_bill_date,
-      pkg         => $hash{date_desc},
+      pkg         => $hash{date_desc} .
+                   ' (' . $hash{quantity} . ' @ $' . $hash{unit_price} . ' ea)',
       taxclass    => $TAXCLASSES{ $hash{taxclass} },
     );
     if (my $classname = $hash{classname}) {
@@ -221,7 +223,7 @@ FILE: foreach my $filename (@$files) {
       $num_errors++;
     } else {
       $num_charges++;
-      $sum_charges += $hash{amount};
+      $sum_charges += $amount;
     }
 
     if ( $opt{e} and $is_e911{$hash{classname}} ) {
diff --git a/FS/bin/freeside-phonenum_list b/FS/bin/freeside-phonenum_list
new file mode 100755 (executable)
index 0000000..19b564d
--- /dev/null
@@ -0,0 +1,86 @@
+#!/usr/bin/perl
+
+use strict;
+use vars qw( $opt_c $opt_o $opt_l $opt_p $opt_b $opt_d $opt_s $opt_t );
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup);
+use FS::Conf;
+use FS::Record qw(qsearch);
+use FS::svc_phone;
+
+getopts('colp:b:d:s:t:');
+
+my $user = shift or &usage;
+adminsuidsetup $user;
+
+my $conf = new FS::Conf;
+my $default_locale = $conf->config('locale') || 'en_US';
+
+my %search = ();
+
+$search{payby}        = [ split(/\s*,\s*/, $opt_p) ] if $opt_p;
+$search{balance}      = $opt_b                       if $opt_b;
+$search{balance_days} = $opt_d                       if $opt_d;
+$search{svcpart}      = [ split(/\s*,\s*/, $opt_s) ] if $opt_s;
+$search{cust_status}  = lc($opt_t)                   if $opt_t;
+
+my @svc_phone = qsearch( FS::svc_phone->search(\%search) );
+
+foreach my $svc_phone (@svc_phone) {
+  print $svc_phone->countrycode if $opt_c;
+  print $svc_phone->phonenum;
+  print '@'. $svc_phone->domain if $opt_o;
+  if ( $opt_l ) {
+    my $cust_pkg = $svc_phone->cust_svc->cust_pkg;
+    print ','. ($cust_pkg && $cust_pkg->cust_main->locale || $default_locale);
+  }
+  print "\n";  
+}
+
+sub usage {
+  die "usage: freeside-phonenum_list [ -c ] [ -o ] [ -l ] [ -p payby,payby... ] [ -b balance [ -d balance_days ] ] [ -s svcpart,svcpart... ] username \n";
+}
+
+=head1 NAME
+
+freeside-phonenum_list
+
+=head1 SYNOPSIS
+  freeside-phonenum_list [ -c ] [ -o ] [ -l ] [ -p payby,payby... ] [ -b balance [ -d balance_days ] ] [ -s svcpart,svcpart... ] username
+
+=head1 DESCRIPTION
+
+Command-line tool to list phone numbers.
+
+Display options:
+
+-c: Include country code
+
+-o: Include domain
+
+-l: Include customer locale
+
+Selection options:
+
+-p: Customer payby (CARD, BILL, etc.).  Separate multiple values with commas.
+
+-b: Customer balance over (or equal to) this amount
+
+-d: Customer balance age over this many days 
+
+-s: Service definition (svcpart).  Separate multiple values with commas.
+
+-t: Customer status: prospect, active, ordered, inactive, suspended or cancelled
+
+username: Employee username
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_phone>, L<FS::cust_main>
+
+=cut
+
+1;
+
index 2fd8025..dcc6ac4 100644 (file)
@@ -212,8 +212,10 @@ while (1) {
       # don't put @args in the log, may expose passwords
       $log->info('starting job ('.$ljob->job.')');
       warn 'running "&'. $ljob->job. '('. join(', ', @args). ")\n" if $DEBUG;
+      local $FS::UID::AutoCommit = 0; # so that we can clean up failures
       eval $eval; #throw away return value?  suppose so
       if ( $@ ) {
+        dbh->rollback;
         my %hash = $ljob->hash;
         $hash{'statustext'} = $@;
         if ( $hash{'statustext'} =~ /\/misc\/queued_report/ ) { #use return?
@@ -225,8 +227,10 @@ while (1) {
         my $fjob = new FS::queue( \%hash );
         my $error = $fjob->replace($ljob);
         die $error if $error;
+        dbh->commit; # for the status change only
       } else {
         $ljob->delete;
+        dbh->commit; # for the job itself
       }
 
       if ( UNIVERSAL::can(dbh, 'sprintProfile') ) {
index c10623c..9df313f 100644 (file)
@@ -108,31 +108,7 @@ while (1) {
       if ( $keepalives && $keepalive_count++ > 10 ) {
         $keepalive_count = 0;
         lock_write;
-
         nstore_fd( { _token => '_keepalive' }, $writer );
-
-#commenting izoom stuff out until we can move it to a branch (or just remove)
-#        foreach my $agent ( qsearch( 'agent', { disabled => '' } ) ) {
-#          my $config = qsearchs( 'conf', { name  => 'selfservice-bulk_ftp_dir',
-#                                           agentnum => $agent->agentnum,
-#                               } )
-#            or next;
-#
-#          my $session =
-#            FS::ClientAPI->dispatch( 'Agent/agent_login',
-#                                     { username => $agent->username,
-#                                       password => $agent->_password,
-#                                     }
-#            );
-#
-#          nstore_fd( { _token     => '_ftp_scan',
-#                       dir        => $config->value,
-#                       session_id => $session->{session_id},
-#                     },
-#                     $writer
-#          );
-#        }
-
         unlock_write;
       }
       next;
index b08a840..3d1c2e0 100755 (executable)
@@ -123,6 +123,8 @@ my $cf;
 while ( $cf = $cfsth->fetchrow_hashref ) {
     my $tbl = $cf->{'dbtable'};
     my $name = $cf->{'name'};
+    $name = lc($name) unless driver_name =~ /^mysql/i;
+
     @statements = grep { $_ !~ /^\s*ALTER\s+TABLE\s+(h_|)$tbl\s+DROP\s+COLUMN\s+cf_$name\s*$/i }
                                                                     @statements;
     push @statements, 
diff --git a/FS/bin/freeside-username_list b/FS/bin/freeside-username_list
new file mode 100755 (executable)
index 0000000..5352f02
--- /dev/null
@@ -0,0 +1,84 @@
+#!/usr/bin/perl
+
+use strict;
+use vars qw( $opt_o $opt_l $opt_p $opt_b $opt_d $opt_s $opt_t );
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup);
+use FS::Conf;
+use FS::Record qw(qsearch);
+use FS::svc_acct;
+
+getopts('olp:b:d:s:t:');
+
+my $user = shift or &usage;
+adminsuidsetup $user;
+
+my $conf = new FS::Conf;
+my $default_locale = $conf->config('locale') || 'en_US';
+
+my %search = ();
+
+$search{payby}        = [ split(/\s*,\s*/, $opt_p) ] if $opt_p;
+$search{balance}      = $opt_b                       if $opt_b;
+$search{balance_days} = $opt_d                       if $opt_d;
+$search{svcpart}      = [ split(/\s*,\s*/, $opt_s) ] if $opt_s;
+$search{cust_status}  = lc($opt_t)                   if $opt_t;
+
+my @svc_acct = qsearch( FS::svc_acct->search(\%search) );
+
+foreach my $svc_acct (@svc_acct) {
+  print $svc_acct->username;
+  print '@'. $svc_acct->domain if $opt_o;
+  if ( $opt_l ) {
+    my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+    print ','. ($cust_pkg && $cust_pkg->cust_main->locale || $default_locale);
+  }
+  print "\n";  
+}
+
+sub usage {
+  die "usage: freeside-username_list [ -c ] [ -l ] [ -p payby,payby... ] [ -b balance [ -d balance_days ] ] [ -s svcpart,svcpart... ] username \n";
+}
+
+=head1 NAME
+
+freeside-username_list
+
+=head1 SYNOPSIS
+
+  freeside-username_list [ -c ] [ -l ] [ -p payby,payby... ] [ -b balance [ -d balance_days ] ] [ -s svcpart,svcpart... ] username
+
+=head1 DESCRIPTION
+
+Command-line tool to list usernames.
+
+Display options:
+
+-o: Include domain
+
+-l: Include customer locale
+
+Selection options:
+
+-p: Customer payby (CARD, BILL, etc.).  Separate multiple values with commas.
+
+-b: Customer balance over (or equal to) this amount
+
+-d: Customer balance age over this many days 
+
+-s: Service definition (svcpart).  Separate multiple values with commas.
+
+-t: Customer status: prospect, active, ordered, inactive, suspended or cancelled
+
+username: Employee username
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_acct>, L<FS::cust_main>
+
+=cut
+
+1;
+
index c6c5531..f0c53e6 100755 (executable)
@@ -1,7 +1,7 @@
 #!/bin/sh
 
-if [ $DISPLAY ] ; then
-  wkhtmltopdf $@
-else
+#if [ $DISPLAY ] ; then
+#  wkhtmltopdf $@
+#else
   xvfb-run -- wkhtmltopdf $@
-fi
+#fi
diff --git a/FS/t/cdr_cust_pkg_usage.t b/FS/t/cdr_cust_pkg_usage.t
new file mode 100644 (file)
index 0000000..1e2060e
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cdr_cust_pkg_usage;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/contact_Mixin.t b/FS/t/contact_Mixin.t
new file mode 100644 (file)
index 0000000..89dcc37
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::contact_Mixin;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_pkg_usage.t b/FS/t/cust_pkg_usage.t
new file mode 100644 (file)
index 0000000..23a7b29
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_pkg_usage;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_pkg_msgcat.t b/FS/t/part_pkg_msgcat.t
new file mode 100644 (file)
index 0000000..541c167
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pkg_msgcat;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_pkg_usage.t b/FS/t/part_pkg_usage.t
new file mode 100644 (file)
index 0000000..ba5ccb6
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pkg_usage;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_pkg_usage_class.t b/FS/t/part_pkg_usage_class.t
new file mode 100644 (file)
index 0000000..e46ff06
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pkg_usage_class;
+$loaded=1;
+print "ok 1\n";
diff --git a/INSTALL b/INSTALL
deleted file mode 100644 (file)
index 4ea1678..0000000
--- a/INSTALL
+++ /dev/null
@@ -1,3 +0,0 @@
-See:
-
-http://www.freeside.biz/mediawiki/index.php/Freeside:1.7:Documentation#Installation_and_upgrades
index 010678f..dd7adb0 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -164,11 +164,15 @@ wikiman:
        chmod a+rx ./bin/pod2x
        ./bin/pod2x
 
-install-docs: check-conflicts docs
-       [ -e ${FREESIDE_DOCUMENT_ROOT} ] && mv ${FREESIDE_DOCUMENT_ROOT} ${FREESIDE_DOCUMENT_ROOT}.`date +%Y%m%d%H%M%S` || true
-       cp -r masondocs ${FREESIDE_DOCUMENT_ROOT}
+install-docs: docs
+       #ancient attempt to avoid overwriting customer modifications directly to production web files that's overlived its usefulness
+       #[ -e ${FREESIDE_DOCUMENT_ROOT} ] && mv ${FREESIDE_DOCUMENT_ROOT} ${FREESIDE_DOCUMENT_ROOT}.`date +%Y%m%d%H%M%S` || true
+       #cp -r masondocs ${FREESIDE_DOCUMENT_ROOT}
+       [ -h ${FREESIDE_DOCUMENT_ROOT} ] && rm ${FREESIDE_DOCUMENT_ROOT} || true
+       mkdir -p ${FREESIDE_DOCUMENT_ROOT}
+       cp -r masondocs/* masondocs/.htaccess ${FREESIDE_DOCUMENT_ROOT}
        chown -R freeside:freeside ${FREESIDE_DOCUMENT_ROOT}
-       cp htetc/handler.pl ${MASON_HANDLER}
+       install -D htetc/handler.pl ${MASON_HANDLER}
        perl -p -i -e "\
          s|%%%FREESIDE_EXPORT%%%|${FREESIDE_EXPORT}|g;\
          s'%%%RT_ENABLED%%%'${RT_ENABLED}'g; \
@@ -225,7 +229,7 @@ perl-modules:
          s|%%%DIST_CONF%%%|${DIST_CONF}|g;\
        " blib/script/*
 
-install-perl-modules: check-conflicts perl-modules install-rt-initialdata
+install-perl-modules: perl-modules install-rt-initialdata
        [ -L ${PERL_INC_DEV_KLUDGE}/FS ] \
          && rm ${PERL_INC_DEV_KLUDGE}/FS \
          && mv ${PERL_INC_DEV_KLUDGE}/FS.old ${PERL_INC_DEV_KLUDGE}/FS \
@@ -372,7 +376,7 @@ create-rt: configure-rt
                                     --datafile ${RT_PATH}/etc/initialdata \
        || true
 
-install-rt: check-conflicts
+install-rt: 
        if [ ${RT_ENABLED} -eq 1 ]; then ( cd rt; make install ); fi
        if [ ${RT_ENABLED} -eq 1 ]; then perl -p -i -e "\
          s'%%%RT_DOMAIN%%%'${RT_DOMAIN}'g;\
@@ -412,9 +416,6 @@ clean:
        -cd fs_selfservice/FS-SelfService; \
        make clean
 
-check-conflicts:
-       ! grep -r --exclude='*config.log*' '--exclude=*config.status*' --exclude=gnupg_details_on_output_formats '--exclude=*mason_handler*' '^=======$$' .
-
 #these are probably only useful if you're me...
 
 #release: upload-docs
index d38c848..1dc1659 100755 (executable)
@@ -7,7 +7,7 @@ $dir =~ s/freeside(\/?)/freeside2.3$1/;
 warn $dir;
 
 #$cmd = "diff -u $file $dir/$file";
-$cmd = "diff -u $dir/$file $file";
+$cmd = "diff -ubBw $dir/$file $file";
 print "$cmd\n";
 system($cmd);
 
index 8aa4ac0..1cce461 100755 (executable)
@@ -37,6 +37,7 @@ do {
   my $ns = $part_export->ns_command( 'GET', '/cdr/',
                                        'time_release' => "$time_release,",
                                        '_sort'        => '+time_release',
+                                      '_limit'      => '500', 
                                    );
 
   #loop over them, double check duplicates, insert the rest
index fdf53d9..32a6d7b 100755 (executable)
@@ -1,13 +1,15 @@
 #!/usr/bin/perl
 
 use strict;
-use vars qw( $opt_p );
+use vars qw( $opt_a $opt_p $opt_t $opt_k );
 use Getopt::Std;
 use FS::UID qw(adminsuidsetup);
-use FS::Record qw(qsearchs);
+use FS::Record qw(qsearch qsearchs);
 use FS::cust_main;
+use FS::cust_tag;
+use FS::cust_pkg;
 
-getopts('p:');
+getopts('a:p:t:k:');
 
 my $user = shift or &usage;
 adminsuidsetup $user;
@@ -31,17 +33,41 @@ while (<STDIN>) {
     next;
   }
 
-  if ( $opt_p ) {
-    $cust_main->payby($opt_p);
+  my %cust_tag = ( custnum=>$custnum, tagnum=>$opt_t );
+  if ( $opt_t && ! qsearchs('cust_tag', \%cust_tag) ) {
+    my $cust_tag = new FS::cust_tag \%cust_tag;
+    my $error = $cust_tag->insert;
+    die "$error\n" if $error;
   }
 
-  my $error = $cust_main->replace;
-  die "$error\n" if $error;
+  if ( $opt_p || $opt_a ) {
+    $cust_main->agentnum($opt_a) if $opt_a;
+    $cust_main->payby($opt_p)    if $opt_p;
+
+    my $error = $cust_main->replace;
+    die "$error\n" if $error;
+  }
+
+  if ( $opt_k ) {
+    foreach my $k (split(/\s*,\s*/, $opt_k)) {
+      my($old, $new) = split(/\s*:\s*/, $k);
+      foreach my $cust_pkg ( qsearch('cust_pkg', {
+                                       'custnum' => $cust_main->custnum,
+                                       'pkgpart' => $old,
+                                    })
+                           )
+      {
+        $cust_pkg->pkgpart($new);
+        my $error = $cust_pkg->replace;
+        die "$error\n" if $error;
+      }
+    }
+  }
 
 }
 
 sub usage {
-  die "usage: cust_main-bulk_change -p NEW_PAYBY employee_username <custnums.txt\n";
+  die "usage: cust_main-bulk_change [ -a agentnum ] [ -p NEW_PAYBY ] [ -t tagnum ] [ -k old_pkgpart:new_pkgpart,... ] employee_username <custnums.txt\n";
 }
 
 =head1 NAME
@@ -50,13 +76,19 @@ cust_main-bulk_change
 
 =head1 SYNOPSIS
 
-  cust_main-bulk_change -p NEW_PAYBY username <custnums.txt
+  cust_main-bulk_change [ -a agentnum ] [ -p NEW_PAYBY ] [ -t tagnum ] [ -k old_pkgpart:new_pkgpart,... ] username <custnums.txt
 
 =head1 DESCRIPTION
 
-Command-line tool to change the payby field for a group of customers.
+Command-line tool to make bulk changes to a group of customers.
+
+-a: new agentnum
+
+-p: new payby, for example, I<CARD> or I<DCRD>
+
+-t: tagnum to add if not present
 
--p: new payby, for example, I<CARD> or I<DCRD>.
+-k: old_pkgpart:new_pkgpart, for example, I<5:4>.  Multiple entries can be comma-separated.
 
 user: Employee username
 
diff --git a/bin/fs-migrate-supplemental b/bin/fs-migrate-supplemental
new file mode 100755 (executable)
index 0000000..dbef95f
--- /dev/null
@@ -0,0 +1,151 @@
+#!/usr/bin/perl
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch qsearchs);
+use FS::cust_pkg;
+use FS::part_pkg;
+
+my $user = shift or die &usage;
+my @pkgparts = @ARGV or die &usage;
+my $dbh = adminsuidsetup $user;
+
+$FS::UID::AutoCommit = 0;
+
+my %stats = (
+  mainpkgs  => 0,
+  created   => 0,
+  linked    => 0,
+  errors    => 0,
+);
+
+my %pkg_freq; # cache
+foreach my $pkgpart (@pkgparts) {
+  my $part_pkg = FS::part_pkg->by_key($pkgpart)
+    or die "pkgpart $pkgpart not found.\n";
+  $pkg_freq{$pkgpart} = $part_pkg->freq;
+  my @links = $part_pkg->supp_part_pkg_link
+    or die "pkgpart $pkgpart has no supplemental packages.\n";
+  CUST_PKG: foreach my $cust_pkg (
+    qsearch('cust_pkg', {
+        'pkgpart' => $pkgpart,
+        'cancel'  => '',
+    })
+  ) {
+    my $cust_main = $cust_pkg->cust_main;
+    my @existing = $cust_pkg->supplemental_pkgs;
+    my @active = grep { !$_->main_pkgnum } $cust_main->ncancelled_pkgs;
+    LINK: foreach my $link (@links) {
+      # yeah, it's expensive
+      # see if there's an existing package with this link identity
+      foreach (@existing) {
+        if ($_->pkglinknum == $link->pkglinknum) {
+          next LINK;
+        }
+      }
+      # no? then is there one with this pkgpart?
+      my $i = 0;
+      foreach (@active) {
+        if ( $_->pkgpart == $link->dst_pkgpart ) {
+          set_link($cust_pkg, $link, $_);
+          splice(@active, $i, 1); # delete it so we don't reuse it
+          next LINK;
+        }
+      }
+      # no? then create one
+      create_linked($cust_pkg, $link);
+    } #foreach $link
+    $stats{mainpkgs}++;
+  } #foreach $cust_pkg
+} #foreach $pkgpart
+
+print "
+Main packages:                 $stats{mainpkgs}
+Supplemental packages linked:  $stats{linked}
+Supplemental packages ordered: $stats{created}
+Errors:                        $stats{errors}
+";
+
+$dbh->commit or die $dbh->errstr;
+
+sub set_link {
+  my ($main_pkg, $part_pkg_link, $supp_pkg) = @_;
+  my $task = "linking package ".$supp_pkg->pkgnum.
+             " to package ".$main_pkg->pkgnum;
+  $supp_pkg->set('main_pkgnum', $main_pkg->pkgnum);
+  $supp_pkg->set('pkglinknum', $part_pkg_link->pkglinknum);
+  # Set the next bill date of the supplemental package to the nearest one in
+  # the future that lines up with the main package.  If the main package
+  # hasn't started billing yet, use its future start date.
+  my $new_bill = $main_pkg->get('bill') || $main_pkg->get('start_date');
+  if ( $new_bill ) {
+    my $old_bill = $supp_pkg->get('bill');
+    my $diff = $new_bill - $old_bill;
+    my $main_freq = $pkg_freq{$main_pkg->pkgpart};
+    my $prev_bill = 0;
+    while ($diff < 0) {
+      # this will exit once $new_bill has overtaken the existing bill date.
+      # if there is no existing bill date, then this will exit right away 
+      # and set bill to the bill date of the main package, which is correct.
+      $prev_bill = $new_bill;
+      $new_bill = FS::part_pkg->add_freq($new_bill, $main_freq);
+      $diff = $new_bill - $old_bill;
+    }
+    # then, of $new_bill and $prev_bill, pick the one that's closer to $old_bill
+    if ( $prev_bill > 0 and 
+         $new_bill - $old_bill > $old_bill - $prev_bill ) {
+      $supp_pkg->set('bill', $prev_bill);
+    } else {
+      $supp_pkg->set('bill', $new_bill);
+    }
+  } else {
+    # otherwise the main package hasn't been billed yet and has no 
+    # start date, so we can't sync the supplemental to it yet.
+    # but we can still link them.
+    warn "$task: main package has no next bill date.\n";
+  }
+  my $error = $supp_pkg->replace;
+  if ( $error ) {
+    warn "$task:\n    $error\n";
+    $stats{errors}++;
+  } else {
+    $stats{linked}++;
+  }
+  return;
+}
+
+sub create_linked {
+  my ($main_pkg, $part_pkg_link) = @_;
+  my $task = "creating pkgpart ".$part_pkg_link->dst_pkgpart.
+             " supplemental to package ".$main_pkg->pkgnum;
+  my $supp_pkg = FS::cust_pkg->new({
+      'pkgpart'       => $part_pkg_link->dst_pkgpart,
+      'pkglinknum'    => $part_pkg_link->pkglinknum,
+      'custnum'       => $main_pkg->custnum,
+      'main_pkgnum'   => $main_pkg->pkgnum,
+      'locationnum'   => $main_pkg->locationnum,
+      'start_date'    => $main_pkg->start_date,
+      'order_date'    => $main_pkg->order_date,
+      'expire'        => $main_pkg->expire,
+      'adjourn'       => $main_pkg->adjourn,
+      'contract_end'  => $main_pkg->contract_end,
+      'susp'          => $main_pkg->susp,
+      'bill'          => $main_pkg->bill,
+      'refnum'        => $main_pkg->refnum,
+      'discountnum'   => $main_pkg->discountnum,
+      'waive_setup'   => $main_pkg->waive_setup,
+  });
+  my $error = $supp_pkg->insert;
+  if ( $error ) {
+    warn "$task:\n    $error\n";
+    $stats{errors}++;
+  } else {
+    $stats{created}++;
+  }
+  return;
+}
+
+sub usage {
+  die "Usage:\n  fs-migrate-supplemental user main_pkgpart\n"; 
+}
+
diff --git a/bin/megapop.pl b/bin/megapop.pl
new file mode 100755 (executable)
index 0000000..e2930fb
--- /dev/null
@@ -0,0 +1,114 @@
+#!/usr/bin/perl -Tw
+#
+# this will break when megapop changes the URL or format of their listing page.
+# that's stupid.  perhaps they can provide a machine-readable listing?
+
+use strict;
+use LWP::UserAgent;
+use FS::UID qw(adminsuidsetup);
+use FS::svc_acct_pop;
+
+my $url = "http://www.megapop.com/location.htm";
+
+my $user = shift or die &usage;
+adminsuidsetup($user);
+
+my %state2usps = &state2usps;
+$state2usps{'WASHINGTON STATE'} = 'WA'; #megapop's on crack
+$state2usps{'CANADA'} = 'CANADA'; #freeside's on crack
+
+my $ua = new LWP::UserAgent;
+my $request = new HTTP::Request('GET', $url);
+my $response = $ua->request($request);
+die $response->error_as_HTML unless $response->is_success;
+my $line;
+my $usps = '';
+foreach $line ( split("\n", $response->content) ) {
+  if ( $line =~ /\W(\w[\w\s]*\w)\s+LOCATIONS/i ) {
+    $usps = $state2usps{uc($1)}
+      or warn "warning: unknown state $1\n";
+  } elsif ( $line =~ /(\d{3})\-(\d{3})\-(\d{4})\s+(\w[\w\s]*\w)/ ) {
+    print "$1 $2 $3 $4 $usps\n";
+    my $svc_acct_pop = new FS::svc_acct_pop ( {
+      'city' => $4,
+      'state' => $usps,
+      'ac' => $1,
+      'exch' => $2,
+    } );
+    my $error = $svc_acct_pop->insert;
+    die $error if $error;
+  }
+}
+
+sub usage {
+  die "Usage:\n  $0 user\n";
+}
+
+sub state2usps{ (
+  'ALABAMA' => 'AL',
+  'ALASKA' => 'AK',
+  'AMERICAN SAMOA' => 'AS',
+  'ARIZONA' => 'AZ',
+  'ARKANSAS' => 'AR',
+  'CALIFORNIA' => 'CA',
+  'COLORADO' => 'CO',
+  'CONNECTICUT' => 'CT',
+  'DELAWARE' => 'DE',
+  'DISTRICT OF COLUMBIA' => 'DC',
+  'FEDERATED STATES OF MICRONESIA' => 'FM',
+  'FLORIDA' => 'FL',
+  'GEORGIA' => 'GA',
+  'GUAM' => 'GU',
+  'HAWAII' => 'HI',
+  'IDAHO' => 'ID',
+  'ILLINOIS' => 'IL',
+  'INDIANA' => 'IN',
+  'IOWA' => 'IA',
+  'KANSAS' => 'KS',
+  'KENTUCKY' => 'KY',
+  'LOUISIANA' => 'LA',
+  'MAINE' => 'ME',
+  'MARSHALL ISLANDS' => 'MH',
+  'MARYLAND' => 'MD',
+  'MASSACHUSETTS' => 'MA',
+  'MICHIGAN' => 'MI',
+  'MINNESOTA' => 'MN',
+  'MISSISSIPPI' => 'MS',
+  'MISSOURI' => 'MO',
+  'MONTANA' => 'MT',
+  'NEBRASKA' => 'NE',
+  'NEVADA' => 'NV',
+  'NEW HAMPSHIRE' => 'NH',
+  'NEW JERSEY' => 'NJ',
+  'NEW MEXICO' => 'NM',
+  'NEW YORK' => 'NY',
+  'NORTH CAROLINA' => 'NC',
+  'NORTH DAKOTA' => 'ND',
+  'NORTHERN MARIANA ISLANDS' => 'MP',
+  'OHIO' => 'OH',
+  'OKLAHOMA' => 'OK',
+  'OREGON' => 'OR',
+  'PALAU' => 'PW',
+  'PENNSYLVANIA' => 'PA',
+  'PUERTO RICO' => 'PR',
+  'RHODE ISLAND' => 'RI',
+  'SOUTH CAROLINA' => 'SC',
+  'SOUTH DAKOTA' => 'SD',
+  'TENNESSEE' => 'TN',
+  'TEXAS' => 'TX',
+  'UTAH' => 'UT',
+  'VERMONT' => 'VT',
+  'VIRGIN ISLANDS' => 'VI',
+  'VIRGINIA' => 'VA',
+  'WASHINGTON' => 'WA',
+  'WEST VIRGINIA' => 'WV',
+  'WISCONSIN' => 'WI',
+  'WYOMING' => 'WY',
+  'ARMED FORCES AFRICA' => 'AE',
+  'ARMED FORCES AMERICAS' => 'AA',
+  'ARMED FORCES CANADA' => 'AE',
+  'ARMED FORCES EUROPE' => 'AE',
+  'ARMED FORCES MIDDLE EAST' => 'AE',
+  'ARMED FORCES PACIFIC' => 'AP',
+) }
+
index 567385b..cd34827 100644 (file)
             $OUT .=  '<th align="center">' . emt('Ref') . '</th>'.
                      '<th align="left">' . emt('Description') . '</th>'.
                      ( $unitprices 
-                       ? '<th align="left">' . emt('Unit Price') . '</th>'.
-                         '<th align="left">' . emt('Quantity') . '</th>'
+                       ? '<th align="right">' . emt('Unit Price') . '</th>'.
+                         '<th align="right">' . emt('Quantity') . '</th>'
                         : '' ).
                      '<th align="right">' . emt('Amount') . '</th>';
           }
                        ( $line->{'ref'} ne $lastref ? $line->{'ref'} : '' ). '</td>'.
                        '<td align="left">'. $line->{'description'}. '</td>'.
                        ( $unitprices 
-                           ? '<td align="left">'. $line->{'unit_amount'}. '</td>'.
-                             '<td align="left">'. $line->{'quantity'}. '</td>'
+                           ? '<td align="right">'. $line->{'unit_amount'}. '</td>'.
+                             '<td align="right">'. $line->{'quantity'}. '</td>'
                            : ''
                        ).
 
index a06c8ff..a6ea1e9 100644 (file)
         <%= 
           my ($last) = grep { $_->{tax_section} || !$_->{summarized} and !($finance_section && $_->{'description'} eq $finance_section) and $_->{'description'} !~ /^\d+ $/ } reverse @sections;
           
-          foreach my $section ( grep { $_->{tax_section} || !$_->{summarized} and !($finance_section && $_->{'description'} eq $finance_section) and $_->{'description'} !~ /^\d+ $/ } @sections ) {
+          #false laziness w/invoice_latexsummary
+          foreach my $section (
+            grep {
+                       $_->{tax_section} || !$_->{summarized}
+                   and ! $_->{adjust_section}
+                   and !($finance_section && $_->{'description'} eq $finance_section)
+                   and $_->{'description'} !~ /^\d+ $/
+                 }
+               @sections
+          ) {
             $OUT .= '<tr><td><b>'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '</b></td>';
             my $celltype = ($last == $section) ? 'th' : 'td';
             $OUT .= qq(<$celltype align="right"><b>). $section->{'subtotal'}. "</b></$celltype></tr>";
           <td><b>New Charges</b></td>
           <th align="right"><b><%= $dollar.$current_less_finance %></b></th>
         </tr>
+
+        <%= 
+          
+          #false laziness w/invoice_latexsummary and above
+          foreach my $section ( grep $_->{adjust_section}, @sections) {
+            $OUT .= '<tr><td><b>'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '</b></td>';
+            $OUT .= qq(<th align="right"><b>). $section->{'subtotal'}. "</b></th></tr>";
+          }
+        %>
+
         <tr>
           <td><b>Total Amount Due</b></td>
-          <td align="right"><b><%= $dollar.sprintf('%.2f', $true_previous_balance + $current_charges - $balance_adjustments) %></b></td>
+          <td align="right"><b><%= $dollar.sprintf('%.2f', $balance) %></b></td>
         </tr>
         <tr><th colspan=2><br></th></tr>
       </table>
index d56a7fb..533e834 100644 (file)
 \newcommand{\FSdescriptionlength} { [@-- $unitprices ? '8.2cm' : '12.8cm' --@] }\r
 \newcommand{\FSdescriptioncolumncount} { [@-- $unitprices ? '4' : '6' --@] }\r
 \newcommand{\FSunitcolumns}{ [@-- \r
-  $unitprices \r
-  ? '\makebox[2.5cm][l]{\textbf{~~'.emt('Unit Price').'}}&\makebox[1.4cm]{\textbf{~'.emt('Quantity').'}}&' \r
+  $unitprices\r
+  ? '\makebox[2.5cm][r]{\textbf{~~' . emt('Unit Price') . '}} &' .\r
+    '\makebox[1.4cm]{\textbf{~' . emt('Quantity') . '}} & ' \r
   : '' --@] }\r
 \r
 \newcommand{\FShead}{\r
 \newcommand{\FSdesc}[5]{\r
   \multicolumn{1}{c}{\rule{0pt}{2.5ex}\textbf{#1}} &\r
   \multicolumn{[@-- $unitprices ? '4' : '6' --@]}{l}{\textbf{#2}} &\r
-[@-- $unitprices ? '  \multicolumn{1}{l}{\textbf{#3}} &'."\n".\r
+[@-- $unitprices ? '  \multicolumn{1}{r}{\textbf{\dollar #3}} &'."\n".\r
                    '  \multicolumn{1}{r}{\textbf{#4}} &'."\n"\r
                  : ''\r
 --@]\r
index 4e4f62b..a68e5d3 100644 (file)
 \textbf{\underline{Summary of New Charges}} & \\
 &\\
 [@--
-  foreach my $section ( grep { $_->{tax_section} || !$_->{summarized} and !($finance_section && $_->{'description'} eq $finance_section) and $_->{'description'} !~ /^\d+ $/ } @sections ) {
+  #false laziness w/invoice_htmlsummary
+  foreach my $section (
+    grep {
+               $_->{tax_section} || !$_->{summarized}
+           and ! $_->{adjust_section}
+           and !($finance_section && $_->{'description'} eq $finance_section)
+           and $_->{'description'} !~ /^\d+ $/
+         }
+      @sections
+  ) {
     $OUT .= '\textbf{'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '}';
     $OUT .= '&\textbf{'. $section->{'subtotal'}. '}\\\\';
   }
 \textbf{Previous Past Due Charges}&\textbf{\dollar[@-- sprintf('%.2f', $true_previous_balance - $balance_adjustments) --@]}\\
 \textbf{Finance charges on overdue amount}&\textbf{\dollar[@-- $finance_amount --@]}\\
 \textbf{New Charges}&\textbf{\dollar[@-- $current_less_finance --@]}\\
+
+[@--
+  #false laziness w/invoice_htmlsummary and above
+  foreach my $section ( grep $_->{adjust_section}, @sections ) {
+    $OUT .= '\textbf{'. ($section->{'description'} ? $section->{'description'} : 'Charges' ). '}';
+    $OUT .= '&\textbf{'. $section->{'subtotal'}. '}\\\\';
+  }
+--@]
+
 \cline{2-2}
-\textbf{Total Amount Due}&\textbf{\dollar[@-- sprintf('%.2f', $true_previous_balance + $current_charges - $balance_adjustments) --@]}\\
+\textbf{Total Amount Due}&\textbf{\dollar[@-- sprintf('%.2f', $balance) --@]}\\
 &\\
 \hline
 \end{tabular}
diff --git a/debian/OLD/config b/debian/OLD/config
new file mode 100644 (file)
index 0000000..4ffa236
--- /dev/null
@@ -0,0 +1,19 @@
+#!/bin/sh
+# config script for freeside
+
+set -e
+
+# source debconf stuff
+. /usr/share/debconf/confmodule
+
+# source dbconfig-common shell library, and call the hook function
+if [ -f /usr/share/dbconfig-common/dpkg/config ]; then
+   # we support mysql and pgsql
+   dbc_dbtypes="pgsql, mysql"
+
+   # source dbconfig-common stuff
+   . /usr/share/dbconfig-common/dpkg/config 
+   dbc_go freeside $@
+fi
+
+# ... rest of your code ...
diff --git a/debian/OLD/cron.d b/debian/OLD/cron.d
new file mode 100644 (file)
index 0000000..f86db1b
--- /dev/null
@@ -0,0 +1,4 @@
+#
+# Regular cron jobs for the freeside package
+#
+0 0    * * *   freeside        /usr/bin/freeside-daily fs_daily
diff --git a/debian/OLD/dbconfig-common.install b/debian/OLD/dbconfig-common.install
new file mode 100644 (file)
index 0000000..31b5d14
--- /dev/null
@@ -0,0 +1,90 @@
+#!/bin/sh
+
+. /etc/dbconfig-common/freeside.conf
+
+DB_USER=$dbc_dbuser
+DB_PASSWORD=$dbc_dbpass
+
+# -- can't find a better place to hook this in.  dammit.
+
+[ "$dbc_dbtype" = "pgsql" ] && DB_TYPE=Pg
+[ "$dbc_dbtype" = "mysql" ] && DB_TYPE=mysql
+#XXX ask dbc about a remote database etc.
+DATASOURCE=DBI:${DB_TYPE}:dbname=${dbc_dbname}
+    
+#debian/rules
+FREESIDE_CONF=/etc/freeside
+FREESIDE_CACHE=/var/cache/freeside
+#XXX huh?
+FREESIDE_EXPORT=/var/spool/freeside
+DEFAULT_CONF=/usr/share/freeside/default_conf
+    
+#XXX this rather seriously needs proper debian-style config file handling.
+    
+#shamelessly lifted from Makefile create-config target
+[ -e ${FREESIDE_CONF} ] || install -d -o freeside ${FREESIDE_CONF}
+    
+touch ${FREESIDE_CONF}/secrets
+chown freeside ${FREESIDE_CONF}/secrets
+chmod 600 ${FREESIDE_CONF}/secrets
+    
+[ -s ${FREESIDE_CONF}/secrets ] || echo -e "${DATASOURCE}\n${DB_USER}\n${DB_PASSWORD}" >${FREESIDE_CONF}/secrets
+chmod 600 ${FREESIDE_CONF}/secrets
+chown freeside ${FREESIDE_CONF}/secrets
+    
+#XXX yuck!  this too!
+[ -e /var/opt/freeside/rt/etc/RT_Config.pm.dbc ] || cp /var/opt/freeside/rt/etc/RT_Config.pm.dbc.generic /var/opt/freeside/rt/etc/RT_Config.pm.dbc
+perl -pi.generic -e "s/^\\s*Set\\s*\\(\s*\\\$DatabaseType.*\$/Set(\\\$DatabaseType, '$DB_TYPE');/" /var/opt/freeside/rt/etc/RT_Config.pm.dbc
+mv /var/opt/freeside/rt/etc/RT_Config.pm.dbc /var/opt/freeside/rt/etc/RT_Config.pm
+perl -pi -e "\
+  s'_DBC_DBUSER_'${dbc_dbuser}'g;\
+  s'_DBC_DBPASS_'${dbc_dbpass}'g;\
+  s'_DBC_DBNAME_'${dbc_dbname}'g;\
+" /var/opt/freeside/rt/etc/RT_Config.pm
+#dunno how to hook this in where i need it...
+#dbc_generate_include="template:/var/opt/freeside/rt/etc/RT_Config.pm"
+#dbc_generate_include_args="-o template_infile=/var/opt/freeside/rt/etc/RT_Config.pm.dbc"
+           
+install -o freeside -d "${FREESIDE_CACHE}/counters.${DATASOURCE}"
+install -o freeside -d "${FREESIDE_CACHE}/cache.${DATASOURCE}"
+install -o freeside -d "${FREESIDE_EXPORT}/export.${DATASOURCE}"
+           
+if [ ! -d "${FREESIDE_CONF}/conf.${DATASOURCE}" ] ; then #don't clobber conf
+install -o freeside -d "${FREESIDE_CONF}/conf.${DATASOURCE}"
+#cp conf/[a-z]* "${FREESIDE_CONF}/conf.${DATASOURCE}"
+cp -i `ls -d ${DEFAULT_CONF}/[a-z]* | grep -v CVS` "${FREESIDE_CONF}/conf.${DATASOURCE}" #-i just in case
+chown -R freeside "${FREESIDE_CONF}/conf.${DATASOURCE}"
+fi
+       
+# -- back to your regularly schedule program... go ahead, create the db
+
+DOMAIN=`dnsdomainname`
+if [ "$DOMAIN" = "localdomain" ]; then #freeside needs a valid domain
+  DOMAIN='example.com'
+fi
+
+# XXX this should probably be handled by the _install_...
+# dpkg-statoverride or something
+chown freeside /etc/freeside
+
+su freeside -c "/usr/bin/freeside-setup -d $DOMAIN"
+su freeside -c '/usr/bin/freeside-adduser -g 1 fs_queue'
+su freeside -c '/usr/bin/freeside-adduser -g 1 fs_daily'
+su freeside -c '/usr/bin/freeside-adduser -g 1 fs_selfservice'
+su freeside -c '/usr/bin/freeside-adduser -g 1 fs_upgrade'
+
+#RT paths are bunk for deb proper
+
+chown freeside /var/opt/freeside/rt/etc/RT_Config.pm
+
+su freeside -c "/var/opt/freeside/rt/sbin/rt-setup-database --dba '$DB_USER' --dba-password '$DB_PASSWORD' --action schema"
+
+su freeside -c '/var/opt/freeside/rt/sbin/rt-setup-database --action insert_initial'
+
+su freeside -c '/var/opt/freeside/rt/sbin/rt-setup-database --action insert --datafile /var/opt/freeside/rt/etc/initialdata'
+
+#XXX this totally doesn't belong here, but what the hey
+chown -R freeside /var/cache/freeside/masondata
+
+exit 0
diff --git a/debian/OLD/dbconfig-common.upgrade b/debian/OLD/dbconfig-common.upgrade
new file mode 100644 (file)
index 0000000..cae9adb
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+su freeside -c '/usr/bin/freeside-upgrade fs_upgrade'
+#RT upgrade
diff --git a/debian/OLD/freeside.apache-alias.conf b/debian/OLD/freeside.apache-alias.conf
new file mode 100644 (file)
index 0000000..fdd4340
--- /dev/null
@@ -0,0 +1 @@
+Alias /freeside/ /usr/share/freeside/www/
diff --git a/debian/OLD/postinst b/debian/OLD/postinst
new file mode 100644 (file)
index 0000000..5d04550
--- /dev/null
@@ -0,0 +1,54 @@
+#!/bin/sh
+# postinst script for freeside
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# source debconf stuff
+. /usr/share/debconf/confmodule
+
+# source dbconfig-common stuff
+. /usr/share/dbconfig-common/dpkg/postinst 
+
+dbc_pgsql_createdb_encoding='sql_ascii'
+
+#echo "i should create the db here"
+dbc_go freeside $@
+#echo "db should be craeted now"
+
+# summary of how this script can be called:
+#        * <postinst> `configure' <most-recently-configured-version>
+#        * <old-postinst> `abort-upgrade' <new version>
+#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>
+#          <new-version>
+#        * <postinst> `abort-remove'
+#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
+#          <failed-install-package> <version> `removing'
+#          <conflicting-package> <version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+case "$1" in
+    configure)
+
+    a2enmod perl
+
+    ;;
+
+    abort-upgrade|abort-remove|abort-deconfigure)
+    ;;
+
+    *)
+        echo "postinst called with unknown argument \`$1'" >&2
+        exit 1
+    ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
+
diff --git a/debian/OLD/postrm b/debian/OLD/postrm
new file mode 100644 (file)
index 0000000..c008445
--- /dev/null
@@ -0,0 +1,48 @@
+#!/bin/sh
+# postrm script for freeside
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# source debconf stuff
+. /usr/share/debconf/confmodule
+
+# source dbconfig-common stuff
+if [ -f /usr/share/dbconfig-common/dpkg/postrm ]; then
+  . /usr/share/dbconfig-common/dpkg/postrm 
+  dbc_go freeside $@
+fi
+
+# summary of how this script can be called:
+#        * <postrm> `remove'
+#        * <postrm> `purge'
+#        * <old-postrm> `upgrade' <new-version>
+#        * <new-postrm> `failed-upgrade' <old-version>
+#        * <new-postrm> `abort-install'
+#        * <new-postrm> `abort-install' <old-version>
+#        * <new-postrm> `abort-upgrade' <old-version>
+#        * <disappearer's-postrm> `disappear' <overwriter>
+#          <overwriter-version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+    purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
+    ;;
+
+    *)
+        echo "postrm called with unknown argument \`$1'" >&2
+        exit 1
+    ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
+
+
diff --git a/debian/OLD/prerm b/debian/OLD/prerm
new file mode 100644 (file)
index 0000000..4c17489
--- /dev/null
@@ -0,0 +1,46 @@
+#!/bin/sh
+# prerm script for freeside
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# source debconf stuff
+. /usr/share/debconf/confmodule
+# source dbconfig-common stuff
+. /usr/share/dbconfig-common/dpkg/prerm 
+dbc_go freeside $@
+
+# summary of how this script can be called:
+#        * <prerm> `remove'
+#        * <old-prerm> `upgrade' <new-version>
+#        * <new-prerm> `failed-upgrade' <old-version>
+#        * <conflictor's-prerm> `remove' `in-favour' <package> <new-version>
+#        * <deconfigured's-prerm> `deconfigure' `in-favour'
+#          <package-being-installed> <version> `removing'
+#          <conflicting-package> <version>
+# for details, see http://www.debian.org/doc/debian-policy/ or
+# the debian-policy package
+
+
+case "$1" in
+    remove|upgrade|deconfigure)
+    ;;
+
+    failed-upgrade)
+    ;;
+
+    *)
+        echo "prerm called with unknown argument \`$1'" >&2
+        exit 1
+    ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
+
+
index 15fed69..d2928e6 100644 (file)
@@ -1,20 +1,57 @@
+--- High ---
+
+web stuff going to /var/www/freeside/masondocs oops
+
+apache configs going to
+./etc/freeside/apache2/freeside-rt.conf
+??  oh there's links etc.  check
+
+file
+./and..?/
+in freeside-lib?  oops wtf
+also
+./default_conf/
+and
+/#for/
+
+test actually installing!
+- FS files
+- /var/www/ files
+- what else should package install?
+  - init script
+  - apache config
+  - /usr/local/etc/freeside/default_conf for new installs
+
+test RT was missing, but we're cheating more now by ignoring a huge remap
+to deb policy-comliant paths.  get it working
+
+init.d/freeside-init
+htetc/handler.pl
+
+#copied to /usr/local/etc/freeside by make install-docs
+htetc/htpasswd.logout
+
+init.d/insserv-override-apache2
+
+etc/longtable.sty
+
+--- Medium --- 
 
 test) freeside-webui /etc/apache/conf.d/freeside.conf
   AuthUserFile is wrong (just fucked)
 
-test its working) somes sort of Alias /freeside /usr/share/freeside/www is needed
-
 test in postinst) freeside package var/cache/freeside/cache.<datasrc is missing>
 
-test RT is missing.  doh.  get it working.
-
-test actually installing!
+--- Low ---
 
---- rc2... right? ---
+bin/* ?  Anything here needed in a live customer install should be moved to FS/bin so it installs as part of the packaging.
 
 freeside-selfservice-client doesn't install at all
 
-start freeside-sqlradius-radacctd from /etc/default/freeside too
+--- Debian ---
+
+redo & test its working) somes sort of Alias /freeside /usr/share/freeside/www is needed
+/var/www/freeside -> /usr/lib/freeside and Alias in apache
 
 Added to README.Debian... do something else?
 Ensure apache is set to run as User freeside.
@@ -24,15 +61,9 @@ init.d.ex or init.d.lsb.ex
 
 finish 
 
-RT install locations (or for now: disable for unstable, enable for
-experiemental.  but try to get it finished off in time for lenny)
+RT install locations (?  maybe our RT libraries shouldn't conflict with
+upstream ones?)
 
 debian/copyright administrivia
 
-AGPL drama
-
 upload
-
-AGPL drama or silent waiting for days or years
-
-profit!  err
index d070c46..0aadb48 100644 (file)
@@ -1,3 +1,9 @@
+freeside (3.0~20130205-1) UNRELEASED; urgency=low
+
+  * Another stab at packaging.
+
+ -- Ivan Kohler <ivan-debian@420.am>  Tue, 05 Feb 2013 17:00:36 -0800
+
 freeside (2.1.1-1) UNRELEASED; urgency=low
 
   * New upstream release
index 7ed6ff8..45a4fb7 100644 (file)
@@ -1 +1 @@
-5
+8
diff --git a/debian/config b/debian/config
deleted file mode 100644 (file)
index 4ffa236..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/sh
-# config script for freeside
-
-set -e
-
-# source debconf stuff
-. /usr/share/debconf/confmodule
-
-# source dbconfig-common shell library, and call the hook function
-if [ -f /usr/share/dbconfig-common/dpkg/config ]; then
-   # we support mysql and pgsql
-   dbc_dbtypes="pgsql, mysql"
-
-   # source dbconfig-common stuff
-   . /usr/share/dbconfig-common/dpkg/config 
-   dbc_go freeside $@
-fi
-
-# ... rest of your code ...
index 4ea4815..1572406 100644 (file)
@@ -5,20 +5,22 @@ Maintainer: Ivan Kohler <ivan-debian@420.am>
 Build-Depends: debhelper (>= 5), perl (>= 5.8)
 Standards-Version: 3.7.2
 Homepage: http://www.freeside.biz/freeside
-Vcs-Browser: http://www.freeside.biz/cgi-bin/viewvc.cgi/freeside/
-Vcs-Cvs: :pserver:anonymous:anonymous@cvs.420.am:/home/cvs/cvsroot freeside
+#Vcs-Browser: http://www.freeside.biz/cgi-bin/viewvc.cgi/freeside/
+#Vcs-Cvs: :pserver:anonymous:anonymous@cvs.420.am:/home/cvs/cvsroot freeside
 
 Package: freeside
 Architecture: all
-Pre-Depends: freeside-lib, dbconfig-common
+Pre-Depends: freeside-lib
+# dbconfig-common
 Depends: ${perl:Depends}, ${shlibs:Depends}, ${misc:Depends}, freeside-webui, debconf, adduser (>= 3.11)
 Recommends: cron
 Suggests: gnupg
 Description: Billing and trouble ticketing for service providers
- Freeside is a web-based billing and trouble ticketing application.  It
- includes features for ISPs, hosting providers, and VoIP providers, but can
- also be used as a generic customer database, invoicing and membership
- application.  If you like buzzwords, call it an "BSS/OSS and CRM solution".
+ Freeside is a web-based billing, trouble ticketing and network monitoring
+ application.  It includes features for ISPs and WISPs, hosting providers and
+ VoIP providers, but can also be used as a generic customer database, invoicing
+ and membership application.  If you like buzzwords, you can call it a
+ "BSS/OSS and CRM solution".
 
 Package: freeside-lib
 Architecture: all
@@ -28,7 +30,9 @@ Suggests: libbusiness-onlinepayment-perl
 Description: Libraries for Freeside billing and trouble ticketing
  Freeside is a web-based billing and trouble ticketing application.
  .
- This package provides the perl libraries and command line utilities.
+ This package provides the perl libraries and command line utilities.  Also,
+ the init script and daemons used by the system are currently provided by this
+ package.
 
 #Package: freeside-bin
 #Architecture: all
index c409cb9..e521a70 100644 (file)
@@ -9,7 +9,7 @@ Upstream Author(s):
 
 Copyright: 
 
-Copyright (C) 2005-2008 Freeside Internet Services, Inc.
+Copyright (C) 2005-2013 Freeside Internet Services, Inc.
 Copyright (C) 2000-2005 Ivan Kohler
 Copyright (C) 1999 Silicon Interactive Software Design
 All rights reserved
diff --git a/debian/cron.d b/debian/cron.d
deleted file mode 100644 (file)
index f86db1b..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-#
-# Regular cron jobs for the freeside package
-#
-0 0    * * *   freeside        /usr/bin/freeside-daily fs_daily
diff --git a/debian/dbconfig-common.install b/debian/dbconfig-common.install
deleted file mode 100644 (file)
index 31b5d14..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/bin/sh
-
-. /etc/dbconfig-common/freeside.conf
-
-DB_USER=$dbc_dbuser
-DB_PASSWORD=$dbc_dbpass
-
-# -- can't find a better place to hook this in.  dammit.
-
-[ "$dbc_dbtype" = "pgsql" ] && DB_TYPE=Pg
-[ "$dbc_dbtype" = "mysql" ] && DB_TYPE=mysql
-#XXX ask dbc about a remote database etc.
-DATASOURCE=DBI:${DB_TYPE}:dbname=${dbc_dbname}
-    
-#debian/rules
-FREESIDE_CONF=/etc/freeside
-FREESIDE_CACHE=/var/cache/freeside
-#XXX huh?
-FREESIDE_EXPORT=/var/spool/freeside
-DEFAULT_CONF=/usr/share/freeside/default_conf
-    
-#XXX this rather seriously needs proper debian-style config file handling.
-    
-#shamelessly lifted from Makefile create-config target
-[ -e ${FREESIDE_CONF} ] || install -d -o freeside ${FREESIDE_CONF}
-    
-touch ${FREESIDE_CONF}/secrets
-chown freeside ${FREESIDE_CONF}/secrets
-chmod 600 ${FREESIDE_CONF}/secrets
-    
-[ -s ${FREESIDE_CONF}/secrets ] || echo -e "${DATASOURCE}\n${DB_USER}\n${DB_PASSWORD}" >${FREESIDE_CONF}/secrets
-chmod 600 ${FREESIDE_CONF}/secrets
-chown freeside ${FREESIDE_CONF}/secrets
-    
-#XXX yuck!  this too!
-[ -e /var/opt/freeside/rt/etc/RT_Config.pm.dbc ] || cp /var/opt/freeside/rt/etc/RT_Config.pm.dbc.generic /var/opt/freeside/rt/etc/RT_Config.pm.dbc
-perl -pi.generic -e "s/^\\s*Set\\s*\\(\s*\\\$DatabaseType.*\$/Set(\\\$DatabaseType, '$DB_TYPE');/" /var/opt/freeside/rt/etc/RT_Config.pm.dbc
-mv /var/opt/freeside/rt/etc/RT_Config.pm.dbc /var/opt/freeside/rt/etc/RT_Config.pm
-perl -pi -e "\
-  s'_DBC_DBUSER_'${dbc_dbuser}'g;\
-  s'_DBC_DBPASS_'${dbc_dbpass}'g;\
-  s'_DBC_DBNAME_'${dbc_dbname}'g;\
-" /var/opt/freeside/rt/etc/RT_Config.pm
-#dunno how to hook this in where i need it...
-#dbc_generate_include="template:/var/opt/freeside/rt/etc/RT_Config.pm"
-#dbc_generate_include_args="-o template_infile=/var/opt/freeside/rt/etc/RT_Config.pm.dbc"
-           
-install -o freeside -d "${FREESIDE_CACHE}/counters.${DATASOURCE}"
-install -o freeside -d "${FREESIDE_CACHE}/cache.${DATASOURCE}"
-install -o freeside -d "${FREESIDE_EXPORT}/export.${DATASOURCE}"
-           
-if [ ! -d "${FREESIDE_CONF}/conf.${DATASOURCE}" ] ; then #don't clobber conf
-install -o freeside -d "${FREESIDE_CONF}/conf.${DATASOURCE}"
-#cp conf/[a-z]* "${FREESIDE_CONF}/conf.${DATASOURCE}"
-cp -i `ls -d ${DEFAULT_CONF}/[a-z]* | grep -v CVS` "${FREESIDE_CONF}/conf.${DATASOURCE}" #-i just in case
-chown -R freeside "${FREESIDE_CONF}/conf.${DATASOURCE}"
-fi
-       
-# -- back to your regularly schedule program... go ahead, create the db
-
-DOMAIN=`dnsdomainname`
-if [ "$DOMAIN" = "localdomain" ]; then #freeside needs a valid domain
-  DOMAIN='example.com'
-fi
-
-# XXX this should probably be handled by the _install_...
-# dpkg-statoverride or something
-chown freeside /etc/freeside
-
-su freeside -c "/usr/bin/freeside-setup -d $DOMAIN"
-su freeside -c '/usr/bin/freeside-adduser -g 1 fs_queue'
-su freeside -c '/usr/bin/freeside-adduser -g 1 fs_daily'
-su freeside -c '/usr/bin/freeside-adduser -g 1 fs_selfservice'
-su freeside -c '/usr/bin/freeside-adduser -g 1 fs_upgrade'
-
-#RT paths are bunk for deb proper
-
-chown freeside /var/opt/freeside/rt/etc/RT_Config.pm
-
-su freeside -c "/var/opt/freeside/rt/sbin/rt-setup-database --dba '$DB_USER' --dba-password '$DB_PASSWORD' --action schema"
-
-su freeside -c '/var/opt/freeside/rt/sbin/rt-setup-database --action insert_initial'
-
-su freeside -c '/var/opt/freeside/rt/sbin/rt-setup-database --action insert --datafile /var/opt/freeside/rt/etc/initialdata'
-
-#XXX this totally doesn't belong here, but what the hey
-chown -R freeside /var/cache/freeside/masondata
-
-exit 0
diff --git a/debian/dbconfig-common.upgrade b/debian/dbconfig-common.upgrade
deleted file mode 100644 (file)
index cae9adb..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-su freeside -c '/usr/bin/freeside-upgrade fs_upgrade'
-#RT upgrade
diff --git a/debian/freeside.apache-alias.conf b/debian/freeside.apache-alias.conf
deleted file mode 100644 (file)
index fdd4340..0000000
+++ /dev/null
@@ -1 +0,0 @@
-Alias /freeside/ /usr/share/freeside/www/
index e845566..f4a511b 100644 (file)
@@ -1 +1,2 @@
 README
+AGPL
diff --git a/debian/init.d.ex b/debian/init.d.ex
deleted file mode 100644 (file)
index 2480f51..0000000
+++ /dev/null
@@ -1,157 +0,0 @@
-#! /bin/sh
-#
-# skeleton     example file to build /etc/init.d/ scripts.
-#              This file should be used to construct scripts for /etc/init.d.
-#
-#              Written by Miquel van Smoorenburg <miquels@cistron.nl>.
-#              Modified for Debian 
-#              by Ian Murdock <imurdock@gnu.ai.mit.edu>.
-#               Further changes by Javier Fernandez-Sanguino <jfs@debian.org>
-#
-# Version:     @(#)skeleton  1.9  26-Feb-2001  miquels@cistron.nl
-#
-
-PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
-DAEMON=/usr/sbin/freeside
-NAME=freeside
-DESC=freeside
-
-test -x $DAEMON || exit 0
-
-LOGDIR=/var/log/freeside
-PIDFILE=/var/run/$NAME.pid
-DODTIME=1                   # Time to wait for the server to die, in seconds
-                            # If this value is set too low you might not
-                            # let some servers to die gracefully and
-                            # 'restart' will not work
-
-# Include freeside defaults if available
-if [ -f /etc/default/freeside ] ; then
-       . /etc/default/freeside
-fi
-
-set -e
-
-running_pid()
-{
-    # Check if a given process pid's cmdline matches a given name
-    pid=$1
-    name=$2
-    [ -z "$pid" ] && return 1 
-    [ ! -d /proc/$pid ] &&  return 1
-    cmd=`cat /proc/$pid/cmdline | tr "\000" "\n"|head -n 1 |cut -d : -f 1`
-    # Is this the expected child?
-    [ "$cmd" != "$name" ] &&  return 1
-    return 0
-}
-
-running()
-{
-# Check if the process is running looking at /proc
-# (works for all users)
-
-    # No pidfile, probably no daemon present
-    [ ! -f "$PIDFILE" ] && return 1
-    # Obtain the pid and check it against the binary name
-    pid=`cat $PIDFILE`
-    running_pid $pid $NAME || return 1
-    return 0
-}
-
-force_stop() {
-# Forcefully kill the process
-    [ ! -f "$PIDFILE" ] && return
-    if running ; then
-        kill -15 $pid
-        # Is it really dead?
-        [ -n "$DODTIME" ] && sleep "$DODTIME"s
-        if running ; then
-            kill -9 $pid
-            [ -n "$DODTIME" ] && sleep "$DODTIME"s
-            if running ; then
-                echo "Cannot kill $LABEL (pid=$pid)!"
-                exit 1
-            fi
-        fi
-    fi
-    rm -f $PIDFILE
-    return 0
-}
-
-case "$1" in
-  start)
-       echo -n "Starting $DESC: "
-       start-stop-daemon --start --quiet --pidfile $PIDFILE \
-               --exec $DAEMON -- $DAEMON_OPTS
-        if running then
-            echo "$NAME."
-        else
-            echo " ERROR."
-        fi
-       ;;
-  stop)
-       echo -n "Stopping $DESC: "
-       start-stop-daemon --stop --quiet --pidfile $PIDFILE \
-               --exec $DAEMON
-       echo "$NAME."
-       ;;
-  force-stop)
-       echo -n "Forcefully stopping $DESC: "
-        force_stop
-        if ! running then
-            echo "$NAME."
-        else
-            echo " ERROR."
-        fi
-       ;;
-  #reload)
-       #
-       #       If the daemon can reload its config files on the fly
-       #       for example by sending it SIGHUP, do it here.
-       #
-       #       If the daemon responds to changes in its config file
-       #       directly anyway, make this a do-nothing entry.
-       #
-       # echo "Reloading $DESC configuration files."
-       # start-stop-daemon --stop --signal 1 --quiet --pidfile \
-       #       /var/run/$NAME.pid --exec $DAEMON
-  #;;
-  force-reload)
-       #
-       #       If the "reload" option is implemented, move the "force-reload"
-       #       option to the "reload" entry above. If not, "force-reload" is
-       #       just the same as "restart" except that it does nothing if the
-       #   daemon isn't already running.
-       # check wether $DAEMON is running. If so, restart
-       start-stop-daemon --stop --test --quiet --pidfile \
-               /var/run/$NAME.pid --exec $DAEMON \
-       && $0 restart \
-       || exit 0
-       ;;
-  restart)
-    echo -n "Restarting $DESC: "
-       start-stop-daemon --stop --quiet --pidfile \
-               /var/run/$NAME.pid --exec $DAEMON
-       [ -n "$DODTIME" ] && sleep $DODTIME
-       start-stop-daemon --start --quiet --pidfile \
-               /var/run/$NAME.pid --exec $DAEMON -- $DAEMON_OPTS
-       echo "$NAME."
-       ;;
-  status)
-    echo -n "$LABEL is "
-    if running ;  then
-        echo "running"
-    else
-        echo " not running."
-        exit 1
-    fi
-    ;;
-  *)
-       N=/etc/init.d/$NAME
-       # echo "Usage: $N {start|stop|restart|reload|force-reload}" >&2
-       echo "Usage: $N {start|stop|restart|force-reload|status|force-stop}" >&2
-       exit 1
-       ;;
-esac
-
-exit 0
diff --git a/debian/init.d.lsb.ex b/debian/init.d.lsb.ex
deleted file mode 100644 (file)
index 1223129..0000000
+++ /dev/null
@@ -1,281 +0,0 @@
-#!/bin/sh 
-#
-# Example init.d script with LSB support.
-#
-# Please read this init.d carefully and modify the sections to
-# adjust it to the program you want to run.
-#
-# Copyright (c) 2007 Javier Fernandez-Sanguino <jfs@debian.org>
-#
-# This is free software; you may redistribute it and/or modify
-# it under the terms of the GNU General Public License as
-# published by the Free Software Foundation; either version 2,
-# or (at your option) any later version.
-#
-# This is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License with
-# the Debian operating system, in /usr/share/common-licenses/GPL;  if
-# not, write to the Free Software Foundation, Inc., 59 Temple Place,
-# Suite 330, Boston, MA 02111-1307 USA
-#
-### BEGIN INIT INFO
-# Provides:          freeside
-# Required-Start:    $network $local_fs
-# Required-Stop:     
-# Should-Start:      $named
-# Should-Stop:       
-# Default-Start:     2 3 4 5
-# Default-Stop:      0 1 6
-# Short-Description: <Enter a short description of the sortware>
-# Description:       <Enter a long description of the software>
-#                    <...>
-#                    <...>
-### END INIT INFO
-
-PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
-
-DAEMON=/usr/sbin/freeside # Introduce the server's location here
-NAME=#PACKAGE              # Introduce the short server's name here
-DESC=#PACKAGE              # Introduce a short description here
-LOGDIR=/var/log/freeside  # Log directory to use
-
-PIDFILE=/var/run/$NAME.pid 
-
-test -x $DAEMON || exit 0
-test -x $DAEMON_WRAPPER || exit 0
-
-. /lib/lsb/init-functions
-
-# Default options, these can be overriden by the information
-# at /etc/default/$NAME
-DAEMON_OPTS=""          # Additional options given to the server 
-
-DODTIME=10              # Time to wait for the server to die, in seconds
-                        # If this value is set too low you might not
-                        # let some servers to die gracefully and
-                        # 'restart' will not work
-                        
-LOGFILE=$LOGDIR/$NAME.log  # Server logfile
-#DAEMONUSER=freeside   # Users to run the daemons as. If this value
-                        # is set start-stop-daemon will chuid the server
-
-# Include defaults if available
-if [ -f /etc/default/$NAME ] ; then
-       . /etc/default/$NAME
-fi
-
-# Use this if you want the user to explicitly set 'RUN' in 
-# /etc/default/
-#if [ "x$RUN" != "xyes" ] ; then
-#    log_failure_msg "$NAME disabled, please adjust the configuration to your needs "
-#    log_failure_msg "and then set RUN to 'yes' in /etc/default/$NAME to enable it."
-#    exit 1
-#fi
-
-# Check that the user exists (if we set a user)
-# Does the user exist?
-if [ -n "$DAEMONUSER" ] ; then
-    if getent passwd | grep -q "^$DAEMONUSER:"; then
-        # Obtain the uid and gid
-        DAEMONUID=`getent passwd |grep "^$DAEMONUSER:" | awk -F : '{print $3}'`
-        DAEMONGID=`getent passwd |grep "^$DAEMONUSER:" | awk -F : '{print $4}'`
-    else
-        log_failure_msg "The user $DAEMONUSER, required to run $NAME does not exist."
-        exit 1
-    fi
-fi
-
-
-set -e
-
-running_pid() {
-# Check if a given process pid's cmdline matches a given name
-    pid=$1
-    name=$2
-    [ -z "$pid" ] && return 1 
-    [ ! -d /proc/$pid ] &&  return 1
-    cmd=`cat /proc/$pid/cmdline | tr "\000" "\n"|head -n 1 |cut -d : -f 1`
-    # Is this the expected server
-    [ "$cmd" != "$name" ] &&  return 1
-    return 0
-}
-
-running() {
-# Check if the process is running looking at /proc
-# (works for all users)
-
-    # No pidfile, probably no daemon present
-    [ ! -f "$PIDFILE" ] && return 1
-    pid=`cat $PIDFILE`
-    running_pid $pid $DAEMON_WRAPPER || return 1
-    return 0
-}
-
-start_server() {
-# Start the process using the wrapper
-        if [ -z "$DAEMONUSER" ] ; then
-            start-stop-daemon --start --quiet --pidfile $PIDFILE \
-                        --exec $DAEMON -- $DAEMON_OPTS
-            errcode=$?
-        else
-# if we are using a daemonuser then change the user id
-            start-stop-daemon --start --quiet --pidfile $PIDFILE \
-                        --chuid $DAEMONUSER \
-                        --exec $DAEMON -- $DAEMON_OPTS
-            errcode=$?
-        fi
-       return $errcode
-}
-
-stop_server() {
-# Stop the process using the wrapper
-        if [ -z "$DAEMONUSER" ] ; then
-            start-stop-daemon --stop --quiet --pidfile $PIDFILE \
-                        --exec $DAEMON
-            errcode=$
-        else
-# if we are using a daemonuser then look for process that match
-            start-stop-daemon --stop --quiet --pidfile $PIDFILE \
-                        --user $DAEMONUSER \
-                        --exec $DAEMON
-            errcode=$
-        fi
-
-       return $errcode
-}
-
-reload_server() {
-    [ ! -f "$PIDFILE" ] && return 1
-    pid=`cat $PIDFILE` # This is the daemon's pid
-    # Send a SIGHUP
-    kill -1 $pid
-    return $?
-}
-
-force_stop() {
-# Force the process to die killing it manually
-       [ ! -e "$PIDFILE" ] && return
-       if running ; then
-               kill -15 $pid
-       # Is it really dead?
-               sleep "$DIETIME"s
-               if running ; then
-                       kill -9 $pid
-                       sleep "$DIETIME"s
-                       if running ; then
-                               echo "Cannot kill $NAME (pid=$pid)!"
-                               exit 1
-                       fi
-               fi
-       fi
-       rm -f $PIDFILE
-}
-
-
-case "$1" in
-  start)
-       log_daemon_msg "Starting $DESC " "$NAME"
-        # Check if it's running first
-        if running ;  then
-            log_progress_msg "apparently already running"
-            log_end_msg 0
-            exit 0
-        fi
-        if start_server && running ;  then
-            # It's ok, the server started and is running
-            log_end_msg 0
-        else
-            # Either we could not start it or it is not running
-            # after we did
-            # NOTE: Some servers might die some time after they start,
-            # this code does not try to detect this and might give
-            # a false positive (use 'status' for that)
-            log_end_msg 1
-        fi
-       ;;
-  stop)
-        log_daemon_msg "Stopping $DESC" "$NAME"
-        if running ; then
-            # Only stop the server if we see it running
-            stop_server
-            log_end_msg $?
-        else
-            # If it's not running don't do anything
-            log_progress_msg "apparently not running"
-            log_end_msg 0
-            exit 0
-        fi
-        ;;
-  force-stop)
-        # First try to stop gracefully the program
-        $0 stop
-        if running; then
-            # If it's still running try to kill it more forcefully
-            log_daemon_msg "Stopping (force) $DESC" "$NAME"
-            force_stop
-            log_end_msg $?
-        fi
-       ;;
-  restart|force-reload)
-        log_daemon_msg "Restarting $DESC" "$NAME"
-        stop_server
-        # Wait some sensible amount, some server need this
-        [ -n "$DIETIME" ] && sleep $DIETIME
-        start_server
-        running
-        log_end_msg $?
-       ;;
-  status)
-
-        log_daemon_msg "Checking status of $DESC" "$NAME"
-        if running ;  then
-            log_progress_msg "running"
-            log_end_msg 0
-        else
-            log_progress_msg "apparently not running"
-            log_end_msg 1
-            exit 1
-        fi
-        ;;
-  # Use this if the daemon cannot reload
-  reload)
-        log_warning_msg "Reloading $NAME daemon: not implemented, as the daemon"
-        log_warning_msg "cannot re-read the config file (use restart)."
-        ;;
-  # And this if it cann
-  #reload)
-          #
-          # If the daemon can reload its config files on the fly
-          # for example by sending it SIGHUP, do it here.
-          #
-          # If the daemon responds to changes in its config file
-          # directly anyway, make this a do-nothing entry.
-          #
-          # log_daemon_msg "Reloading $DESC configuration files" "$NAME"
-          # if running ; then
-          #    reload_server
-          #    if ! running ;  then
-          # Process died after we tried to reload
-          #       log_progress_msg "died on reload"
-          #       log_end_msg 1
-          #       exit 1
-          #    fi
-          # else
-          #    log_progress_msg "server is not running"
-          #    log_end_msg 1
-          #    exit 1
-          # fi
-                                                                                    #;;
-
-  *)
-       N=/etc/init.d/$NAME
-       echo "Usage: $N {start|stop|force-stop|restart|force-reload|status}" >&2
-       exit 1
-       ;;
-esac
-
-exit 0
diff --git a/debian/postinst b/debian/postinst
deleted file mode 100644 (file)
index 5d04550..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/bin/sh
-# postinst script for freeside
-#
-# see: dh_installdeb(1)
-
-set -e
-
-# source debconf stuff
-. /usr/share/debconf/confmodule
-
-# source dbconfig-common stuff
-. /usr/share/dbconfig-common/dpkg/postinst 
-
-dbc_pgsql_createdb_encoding='sql_ascii'
-
-#echo "i should create the db here"
-dbc_go freeside $@
-#echo "db should be craeted now"
-
-# summary of how this script can be called:
-#        * <postinst> `configure' <most-recently-configured-version>
-#        * <old-postinst> `abort-upgrade' <new version>
-#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>
-#          <new-version>
-#        * <postinst> `abort-remove'
-#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
-#          <failed-install-package> <version> `removing'
-#          <conflicting-package> <version>
-# for details, see http://www.debian.org/doc/debian-policy/ or
-# the debian-policy package
-
-case "$1" in
-    configure)
-
-    a2enmod perl
-
-    ;;
-
-    abort-upgrade|abort-remove|abort-deconfigure)
-    ;;
-
-    *)
-        echo "postinst called with unknown argument \`$1'" >&2
-        exit 1
-    ;;
-esac
-
-# dh_installdeb will replace this with shell code automatically
-# generated by other debhelper scripts.
-
-#DEBHELPER#
-
-exit 0
-
diff --git a/debian/postrm b/debian/postrm
deleted file mode 100644 (file)
index c008445..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/bin/sh
-# postrm script for freeside
-#
-# see: dh_installdeb(1)
-
-set -e
-
-# source debconf stuff
-. /usr/share/debconf/confmodule
-
-# source dbconfig-common stuff
-if [ -f /usr/share/dbconfig-common/dpkg/postrm ]; then
-  . /usr/share/dbconfig-common/dpkg/postrm 
-  dbc_go freeside $@
-fi
-
-# summary of how this script can be called:
-#        * <postrm> `remove'
-#        * <postrm> `purge'
-#        * <old-postrm> `upgrade' <new-version>
-#        * <new-postrm> `failed-upgrade' <old-version>
-#        * <new-postrm> `abort-install'
-#        * <new-postrm> `abort-install' <old-version>
-#        * <new-postrm> `abort-upgrade' <old-version>
-#        * <disappearer's-postrm> `disappear' <overwriter>
-#          <overwriter-version>
-# for details, see http://www.debian.org/doc/debian-policy/ or
-# the debian-policy package
-
-
-case "$1" in
-    purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
-    ;;
-
-    *)
-        echo "postrm called with unknown argument \`$1'" >&2
-        exit 1
-    ;;
-esac
-
-# dh_installdeb will replace this with shell code automatically
-# generated by other debhelper scripts.
-
-#DEBHELPER#
-
-exit 0
-
-
diff --git a/debian/prerm b/debian/prerm
deleted file mode 100644 (file)
index 4c17489..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/bin/sh
-# prerm script for freeside
-#
-# see: dh_installdeb(1)
-
-set -e
-
-# source debconf stuff
-. /usr/share/debconf/confmodule
-# source dbconfig-common stuff
-. /usr/share/dbconfig-common/dpkg/prerm 
-dbc_go freeside $@
-
-# summary of how this script can be called:
-#        * <prerm> `remove'
-#        * <old-prerm> `upgrade' <new-version>
-#        * <new-prerm> `failed-upgrade' <old-version>
-#        * <conflictor's-prerm> `remove' `in-favour' <package> <new-version>
-#        * <deconfigured's-prerm> `deconfigure' `in-favour'
-#          <package-being-installed> <version> `removing'
-#          <conflicting-package> <version>
-# for details, see http://www.debian.org/doc/debian-policy/ or
-# the debian-policy package
-
-
-case "$1" in
-    remove|upgrade|deconfigure)
-    ;;
-
-    failed-upgrade)
-    ;;
-
-    *)
-        echo "prerm called with unknown argument \`$1'" >&2
-        exit 1
-    ;;
-esac
-
-# dh_installdeb will replace this with shell code automatically
-# generated by other debhelper scripts.
-
-#DEBHELPER#
-
-exit 0
-
-
index d37dfd1..a8835e5 100755 (executable)
@@ -12,30 +12,37 @@ PERL   ?= /usr/bin/perl
 #PACKAGE = $(shell dh_listpackages)
 PACKAGE = freeside
 TMP     = $(CURDIR)/debian/$(PACKAGE)
-DBC_SCRIPTS = $(TMP)/usr/share/dbconfig-common/scripts/freeside
+#DBC_SCRIPTS = $(TMP)/usr/share/dbconfig-common/scripts/freeside
 
-#this is gotten from dbconfig-common
-DB_TYPE = db_type_is_configured_during_pkg_install_by_dbconfig-common_not_at_build_time
+##this is gotten from dbconfig-common
+#DB_TYPE = db_type_is_configured_during_pkg_install_by_dbconfig-common_not_at_build_time
 
 #no chance, it doesn't get backslash-interpolted now...
-#DEBVERSION                    = `head -1 debian/changelog | cut -d')' -f1 | cut -c11-`
-DEBVERSION                    = 1.7.3~rc2-1
-export VERSION                = $(DEBVERSION) (Debian)
+##DEBVERSION                    = `head -1 debian/changelog | cut -d')' -f1 | cut -c11-`
+#DEBVERSION                    = 1.7.3~rc2-1
+#export VERSION                = $(DEBVERSION) (Debian)
 
-export FREESIDE_CONF          = /etc/freeside
-export FREESIDE_LOG           = /var/log/freeside
-export FREESIDE_LOCK          = /var/lock/freeside
-export FREESIDE_CACHE         = $(TMP)/var/cache/freeside
-FREESIDE_CACHE         = $(TMP)/var/cache/freeside
+#export FREESIDE_CONF          = /etc/freeside
+#export FREESIDE_LOG           = /var/log/freeside
+#export FREESIDE_LOCK          = /var/lock/freeside
+#export FREESIDE_CACHE         = $(TMP)/var/cache/freeside
+#FREESIDE_CACHE         = $(TMP)/var/cache/freeside
 
 #XXX huh?
-export FREESIDE_EXPORT        = /var/spool/freeside
+#export FREESIDE_EXPORT        = /var/spool/freeside
+
+export FREESIDE_CONF = $(TMP)/usr/local/etc/freeside
+export FREESIDE_LOG = $(TMP)/usr/local/etc/freeside
+export FREESIDE_LOCK = $(TMP)/usr/local/etc/freeside
+export FREESIDE_CACHE = $(TMP)/usr/local/etc/freeside
+export FREESIDE_EXPORT = $(TMP)/usr/local/etc/freeside
 
 #XXX own subdir?
-export MASON_HANDLER          = $(TMP)-webui/usr/share/freeside/handler.pl
+#export MASON_HANDLER          = $(TMP)-webui/usr/share/freeside/handler.pl
+export MASON_HANDLER=$(TMP)-webui/usr/local/etc/freeside/handler.pl
 
-export APACHE_VERSION         = 2
-export FREESIDE_DOCUMENT_ROOT = $(TMP)-webui/usr/share/freeside/www
+#export FREESIDE_DOCUMENT_ROOT = $(TMP)-webui/usr/share/freeside/www
+export FREESIDE_DOCUMENT_ROOT = $(TMP)-webui/var/www/freeside
 export INIT_FILE              = $(TMP).init
 export INIT_INSTALL           = /bin/true
 export HTTPD_RESTART          = /bin/true
@@ -49,22 +56,22 @@ export INSTALLGROUP           = adm
 export SELFSERVICE_MACHINES   = 
 
 #prompt ?   XXX these are runtime, not buildtime :/
-export RT_DOMAIN              = `dnsdomainname`
-export RT_TIMEZONE            = `cat /etc/timezone`
+#export RT_DOMAIN              = `dnsdomainname`
+#export RT_TIMEZONE            = `cat /etc/timezone`
 
-export HOSTNAME               = `hostname -f`
-export FREESIDE_URL           = http://$(HOSTNAME)/freeside/
+#export HOSTNAME               = `hostname -f`
+#export FREESIDE_URL           = http://$(HOSTNAME)/freeside/
 
 #specific to deb pkg, for purposes of saving off a permanent copy of default
 #config for postinst and that sort of thing
-export DIST_CONF           = $(TMP)/usr/share/freeside/default_conf
+#export DIST_CONF           = $(TMP)/usr/share/freeside/default_conf
 
 #XXX yuck.  proper RT layout is entirely necessary
 #this seems to infect way to much of RT with the build location, requiring
 # a kludge to hack it out afterwords.  look into using fakeroot (didn't
 # realize it would need to be explicit argh)
 # (but leaving it for now, otherwise can't get RT to put files where we need em)
-export RT_PATH                = $(TMP)/var/opt/freeside/rt
+#export RT_PATH                = $(TMP)/var/opt/freeside/rt
 
 # This has to be exported to make some magic below work.
 export DH_OPTIONS
@@ -114,14 +121,14 @@ install-stamp: build-stamp
 
         #false laziness w/install-perl-modules now
        #install this for postinst later (no create-config)
-       install -d $(DIST_CONF)
+       ##install -d $(DIST_CONF)
        #install conf/[a-z]* $(DEFAULT_CONF)
        #CVS is not [a-z]
-       install `ls -d conf/[a-z]* | grep -v CVS` $(DIST_CONF)
+       ##install `ls -d conf/[a-z]* | grep -v CVS` $(DIST_CONF)
 
        install -d $(FREESIDE_DOCUMENT_ROOT)
        install -d $(FREESIDE_CACHE)/masondata #MASONDATA
-       $(MAKE) -e install-docs
+       $(MAKE) -e DESTDIR=$(TMP)-webui install-docs
 
        #hack the build dir out of Freeside too.  oh yeah, sucky.
        perl -p -i -e "\
@@ -131,71 +138,75 @@ install-stamp: build-stamp
          ${TMP}/usr/share/perl5/FS/*/* \
          ${TMP}/usr/bin/*
 
-       rm -r $(FREESIDE_DOCUMENT_ROOT).*
+       #rm -r $(FREESIDE_DOCUMENT_ROOT).*
 
        install -d $(APACHE_CONF)
-       install debian/freeside.apache-alias.conf $(APACHE_CONF)/freeside-alias.conf
-       FREESIDE_DOCUMENT_ROOT=/usr/share/freeside/www MASON_HANDLER=/usr/share/freeside/handler.pl FREESIDE_CONF=/etc/freeside $(MAKE) -e install-apache
+       #install debian/freeside.apache-alias.conf $(APACHE_CONF)/freeside-alias.conf
+       #FREESIDE_DOCUMENT_ROOT=/usr/share/freeside/www MASON_HANDLER=/usr/share/freeside/handler.pl FREESIDE_CONF=/etc/freeside $(MAKE) -e install-apache
+       $(MAKE) -e install-apache
 
        $(MAKE) -e install-init
 
        #RT
        #(configure-rt)
-
-       # XXX need to adjust db-type, db-database, db-rt-user, db-rt-pass
-       # based on info from dbc
-       ( cd rt; \
-         cp config.layout.in config.layout; \
-         perl -p -i -e "\
-           s'%%%FREESIDE_DOCUMENT_ROOT%%%'${FREESIDE_DOCUMENT_ROOT}'g;\
-           s'%%%MASONDATA%%%'${FREESIDE_CACHE}/masondata'g;\
-         " config.layout; \
-         ./configure --prefix=${RT_PATH} \
-                     --enable-layout=Freeside \
-                     --with-db-type=Pg \
-                     --with-db-dba=freeside \
-                     --with-db-database=_DBC_DBNAME_ \
-                     --with-db-rt-user=_DBC_DBUSER_ \
-                     --with-db-rt-pass=_DBC_DBPASS_ \
-                     --with-web-user=freeside \
-                     --with-web-group=freeside \
-                     --with-rt-group=freeside \
-       )
-
-       #(create-rt)
-       install -d $(RT_PATH)
-       ( cd rt; make install )
-       #hack the build dir out of RT.  yeah, sucky.
-       perl -p -i -e "\
-         s'${TMP}''g;\
-       " ${RT_PATH}/etc/RT_Config.pm \
-         ${RT_PATH}/lib/RT.pm \
-         ${RT_PATH}/bin/mason_handler.fcgi \
-         ${RT_PATH}/bin/mason_handler.scgi \
-         ${RT_PATH}/bin/standalone_httpd \
-         ${RT_PATH}/bin/webmux.pl \
-         ${RT_PATH}/bin/rt-crontool \
-         ${RT_PATH}/sbin/rt-dump-database \
-         ${RT_PATH}/sbin/rt-setup-database
-       
-       #hack @INC dir out of RT (well, handler.pl) too.
-       perl -p -i -e "\
-         s'/opt/rt3/'/var/opt/freeside/rt/'g;\
-       " ${TMP}-webui/usr/share/freeside/handler.pl
-
-       mv ${RT_PATH}/etc/RT_Config.pm ${RT_PATH}/etc/RT_Config.pm.dbc
-
-       perl -p -i -e "\
-         s'%%%RT_DOMAIN%%%'${RT_DOMAIN}'g;\
-         s'%%%RT_TIMEZONE%%%'${RT_TIMEZONE}'g;\
-         s'%%%FREESIDE_URL%%%'${FREESIDE_URL}'g;\
-       " ${RT_PATH}/etc/RT_SiteConfig.pm
-
-       install -D debian/dbconfig-common.install $(DBC_SCRIPTS)/install/pgsql
-       install -D debian/dbconfig-common.install $(DBC_SCRIPTS)/install/mysql
+       $(MAKE) -e configure-rt
+
+       ## XXX need to adjust db-type, db-database, db-rt-user, db-rt-pass
+       ## based on info from dbc
+       #( cd rt; \
+       #  cp config.layout.in config.layout; \
+       #  perl -p -i -e "\
+       #    s'%%%FREESIDE_DOCUMENT_ROOT%%%'${FREESIDE_DOCUMENT_ROOT}'g;\
+       #    s'%%%MASONDATA%%%'${FREESIDE_CACHE}/masondata'g;\
+       #  " config.layout; \
+       #  ./configure --prefix=${RT_PATH} \
+       #              --enable-layout=Freeside \
+       #              --with-db-type=Pg \
+       #              --with-db-dba=freeside \
+       #              --with-db-database=_DBC_DBNAME_ \
+       #              --with-db-rt-user=_DBC_DBUSER_ \
+       #              --with-db-rt-pass=_DBC_DBPASS_ \
+       #              --with-web-user=freeside \
+       #              --with-web-group=freeside \
+       #              --with-rt-group=freeside \
+       #)
+
+       ##(create-rt)
+       #$(MAKE) -e create-rt
+
+       #install -d $(RT_PATH)
+       #( cd rt; make install )
+       ##hack the build dir out of RT.  yeah, sucky.
+       #perl -p -i -e "\
+       #  s'${TMP}''g;\
+       #" ${RT_PATH}/etc/RT_Config.pm \
+       #  ${RT_PATH}/lib/RT.pm \
+       #  ${RT_PATH}/bin/mason_handler.fcgi \
+       #  ${RT_PATH}/bin/mason_handler.scgi \
+       #  ${RT_PATH}/bin/standalone_httpd \
+       #  ${RT_PATH}/bin/webmux.pl \
+       #  ${RT_PATH}/bin/rt-crontool \
+       #  ${RT_PATH}/sbin/rt-dump-database \
+       #  ${RT_PATH}/sbin/rt-setup-database
+       #
+       ##hack @INC dir out of RT (well, handler.pl) too.
+       #perl -p -i -e "\
+       #  s'/opt/rt3/'/var/opt/freeside/rt/'g;\
+       #" ${TMP}-webui/usr/share/freeside/handler.pl
+
+       #mv ${RT_PATH}/etc/RT_Config.pm ${RT_PATH}/etc/RT_Config.pm.dbc
+
+       #perl -p -i -e "\
+       #  s'%%%RT_DOMAIN%%%'${RT_DOMAIN}'g;\
+       #  s'%%%RT_TIMEZONE%%%'${RT_TIMEZONE}'g;\
+       #  s'%%%FREESIDE_URL%%%'${FREESIDE_URL}'g;\
+       #" ${RT_PATH}/etc/RT_SiteConfig.pm
+
+       #install -D debian/dbconfig-common.install $(DBC_SCRIPTS)/install/pgsql
+       #install -D debian/dbconfig-common.install $(DBC_SCRIPTS)/install/mysql
        
-       install -D debian/dbconfig-common.upgrade $(DBC_SCRIPTS)/upgrade/pgsql/$(DEBVERSION)
-       install -D debian/dbconfig-common.upgrade $(DBC_SCRIPTS)/upgrade/mysql/$(DEBVERSION)
+       #install -D debian/dbconfig-common.upgrade $(DBC_SCRIPTS)/upgrade/pgsql/$(DEBVERSION)
+       #install -D debian/dbconfig-common.upgrade $(DBC_SCRIPTS)/upgrade/mysql/$(DEBVERSION)
        
        dh_install
 
@@ -207,7 +218,6 @@ binary-arch:
 binary-indep: build install
        dh_testdir
        dh_testroot
-       dh_installchangelogs ChangeLog
        dh_installdocs #freeside.docs README AGPL
        dh_installexamples eg/*
 #      dh_installmenu
diff --git a/debian/templates b/debian/templates
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/etc/megapop.pl b/etc/megapop.pl
deleted file mode 100755 (executable)
index e2930fb..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# this will break when megapop changes the URL or format of their listing page.
-# that's stupid.  perhaps they can provide a machine-readable listing?
-
-use strict;
-use LWP::UserAgent;
-use FS::UID qw(adminsuidsetup);
-use FS::svc_acct_pop;
-
-my $url = "http://www.megapop.com/location.htm";
-
-my $user = shift or die &usage;
-adminsuidsetup($user);
-
-my %state2usps = &state2usps;
-$state2usps{'WASHINGTON STATE'} = 'WA'; #megapop's on crack
-$state2usps{'CANADA'} = 'CANADA'; #freeside's on crack
-
-my $ua = new LWP::UserAgent;
-my $request = new HTTP::Request('GET', $url);
-my $response = $ua->request($request);
-die $response->error_as_HTML unless $response->is_success;
-my $line;
-my $usps = '';
-foreach $line ( split("\n", $response->content) ) {
-  if ( $line =~ /\W(\w[\w\s]*\w)\s+LOCATIONS/i ) {
-    $usps = $state2usps{uc($1)}
-      or warn "warning: unknown state $1\n";
-  } elsif ( $line =~ /(\d{3})\-(\d{3})\-(\d{4})\s+(\w[\w\s]*\w)/ ) {
-    print "$1 $2 $3 $4 $usps\n";
-    my $svc_acct_pop = new FS::svc_acct_pop ( {
-      'city' => $4,
-      'state' => $usps,
-      'ac' => $1,
-      'exch' => $2,
-    } );
-    my $error = $svc_acct_pop->insert;
-    die $error if $error;
-  }
-}
-
-sub usage {
-  die "Usage:\n  $0 user\n";
-}
-
-sub state2usps{ (
-  'ALABAMA' => 'AL',
-  'ALASKA' => 'AK',
-  'AMERICAN SAMOA' => 'AS',
-  'ARIZONA' => 'AZ',
-  'ARKANSAS' => 'AR',
-  'CALIFORNIA' => 'CA',
-  'COLORADO' => 'CO',
-  'CONNECTICUT' => 'CT',
-  'DELAWARE' => 'DE',
-  'DISTRICT OF COLUMBIA' => 'DC',
-  'FEDERATED STATES OF MICRONESIA' => 'FM',
-  'FLORIDA' => 'FL',
-  'GEORGIA' => 'GA',
-  'GUAM' => 'GU',
-  'HAWAII' => 'HI',
-  'IDAHO' => 'ID',
-  'ILLINOIS' => 'IL',
-  'INDIANA' => 'IN',
-  'IOWA' => 'IA',
-  'KANSAS' => 'KS',
-  'KENTUCKY' => 'KY',
-  'LOUISIANA' => 'LA',
-  'MAINE' => 'ME',
-  'MARSHALL ISLANDS' => 'MH',
-  'MARYLAND' => 'MD',
-  'MASSACHUSETTS' => 'MA',
-  'MICHIGAN' => 'MI',
-  'MINNESOTA' => 'MN',
-  'MISSISSIPPI' => 'MS',
-  'MISSOURI' => 'MO',
-  'MONTANA' => 'MT',
-  'NEBRASKA' => 'NE',
-  'NEVADA' => 'NV',
-  'NEW HAMPSHIRE' => 'NH',
-  'NEW JERSEY' => 'NJ',
-  'NEW MEXICO' => 'NM',
-  'NEW YORK' => 'NY',
-  'NORTH CAROLINA' => 'NC',
-  'NORTH DAKOTA' => 'ND',
-  'NORTHERN MARIANA ISLANDS' => 'MP',
-  'OHIO' => 'OH',
-  'OKLAHOMA' => 'OK',
-  'OREGON' => 'OR',
-  'PALAU' => 'PW',
-  'PENNSYLVANIA' => 'PA',
-  'PUERTO RICO' => 'PR',
-  'RHODE ISLAND' => 'RI',
-  'SOUTH CAROLINA' => 'SC',
-  'SOUTH DAKOTA' => 'SD',
-  'TENNESSEE' => 'TN',
-  'TEXAS' => 'TX',
-  'UTAH' => 'UT',
-  'VERMONT' => 'VT',
-  'VIRGIN ISLANDS' => 'VI',
-  'VIRGINIA' => 'VA',
-  'WASHINGTON' => 'WA',
-  'WEST VIRGINIA' => 'WV',
-  'WISCONSIN' => 'WI',
-  'WYOMING' => 'WY',
-  'ARMED FORCES AFRICA' => 'AE',
-  'ARMED FORCES AMERICAS' => 'AA',
-  'ARMED FORCES CANADA' => 'AE',
-  'ARMED FORCES EUROPE' => 'AE',
-  'ARMED FORCES MIDDLE EAST' => 'AE',
-  'ARMED FORCES PACIFIC' => 'AP',
-) }
-
index c22e426..651a8f5 100644 (file)
@@ -57,6 +57,10 @@ $socket .= '.'.$tag if defined $tag && length($tag);
   'svc_status_html'           => 'MyAccount/svc_status_html',
   'svc_status_hash'           => 'MyAccount/svc_status_hash',
   'set_svc_status_hash'       => 'MyAccount/set_svc_status_hash',
+  'set_svc_status_listadd'    => 'MyAccount/set_svc_status_listadd',
+  'set_svc_status_listdel'    => 'MyAccount/set_svc_status_listdel',
+  'set_svc_status_vacationadd'=> 'MyAccount/set_svc_status_vacationadd',
+  'set_svc_status_vacationdel'=> 'MyAccount/set_svc_status_vacationdel',
   'acct_forward_info'         => 'MyAccount/acct_forward_info',
   'process_acct_forward'      => 'MyAccount/process_acct_forward',
   'list_dsl_devices'          => 'MyAccount/list_dsl_devices',   
index c7d2bb2..4a31b12 100644 (file)
@@ -88,11 +88,13 @@ push @menu,
   { title=>'Logout',   url=>'logout', size=>'+1', },
 ;
 
+my %menu_disable = map { $_=>1 } @menu_disable;
 foreach my $item ( @menu ) {
 
-  next if $menu_skipblanks && $item->{'title'} =~ /^\s*$/;
-  next if $menu_skipheadings && ! $item->{'url'};
-
+  next if ( $menu_skipblanks && $item->{'title'} =~ /^\s*$/ )
+       || ( $menu_skipheadings && ! $item->{'url'} )
+       || $menu_disable{$item->{'title'}};
+  
   $OUT .= '<TR><TD'; 
   if ( $menu_body_image ) {
     if ( exists $item->{'url'} && $action eq $item->{'url'} ) {
index de0ab1a..f7fe308 100755 (executable)
@@ -873,6 +873,7 @@ sub view_cdr_details {
     'svcnum'      => $cgi->param('svcnum'),
     'beginning'   => $cgi->param('beginning') || '',
     'ending'      => $cgi->param('ending') || '',
+    'inbound'     => $cgi->param('inbound') || 0,
   );
 }
 
index f33ec49..16ef7f7 100755 (executable)
          ' Signup form</FONT><BR><BR>';
 %>
 
-<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
+<FONT SIZE="+1" COLOR="#ff0000"><%= encode_entities($error) %></FONT>
 
 <FORM NAME="OneTrueForm" ACTION="<%= $self_url %>" METHOD=POST onSubmit="document.OneTrueForm.signup.disabled=true">
-<INPUT TYPE="hidden" NAME="prepaid_shortform" VALUE="<%= $prepaid_shortform %>">
+<INPUT TYPE="hidden" NAME="prepaid_shortform" VALUE="<%= encode_entities($prepaid_shortform) %>">
 <INPUT TYPE="hidden" NAME="session" VALUE="<%= $session_id %>">
 <INPUT TYPE="hidden" NAME="action" VALUE="process_signup">
 <INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agentnum %>">
@@ -149,6 +149,7 @@ $OUT .= qq!
 else {
     @payby = ('PREPAY');
 }
+'';
 %>
 
 <BR>Billing information<TABLE BGCOLOR="<%= $box_bgcolor || '#c0c0c0' %>" BORDER=0 CELLSPACING=0 WIDTH="100%">
@@ -213,7 +214,7 @@ else {
 
     my( $account, $aba ) = split('@', $payinfo);
     my %paybychecked = (
-      'CARD' => '<TABLE BGCOLOR="'. ( $box_bgcolor || '#c0c0c0' ). qq!" BORDER=0 CELLSPACING=0 WIDTH="100%"><TR><TD ALIGN="right"><font color="#ff0000">*</font> Card type</TD><TD>$cardselect</TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Card number</TD><TD><INPUT TYPE="text" NAME="CARD_payinfo" VALUE="$payinfo" MAXLENGTH=19></TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Expration</TD><TD>!. expselect("CARD", $paydate). qq!</TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Name on card</TD><TD><INPUT TYPE="text" NAME="CARD_payname" VALUE="$payname"></TD></TR>!,
+      'CARD' => '<TABLE BGCOLOR="'. ( $box_bgcolor || '#c0c0c0' ). qq!" BORDER=0 CELLSPACING=0 WIDTH="100%"><TR><TD ALIGN="right"><font color="#ff0000">*</font> Card type</TD><TD>$cardselect</TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Card number</TD><TD><INPUT TYPE="text" NAME="CARD_payinfo" VALUE="$payinfo" MAXLENGTH=19></TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Expiration</TD><TD>!. expselect("CARD", $paydate). qq!</TD></TR><TR><TD ALIGN="right"><font color="#ff0000">*</font> Name on card</TD><TD><INPUT TYPE="text" NAME="CARD_payname" VALUE="$payname"></TD></TR>!,
       'DCRD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="DCRD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("DCRD", $paydate). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="DCRD_payname" VALUE="$payname">!,
       'CHEK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE="$account" MAXLENGTH=10> Type <SELECT NAME="CHEK_paytype">!. join('', map {qq!<OPTION VALUE="$_"!.($paytype eq $_ ? ' SELECTED' : '').">$_</OPTION>"} @paytypes). qq!</SELECT><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="$payname">!,
       'DCHK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE="$account" MAXLENGTH=10> Type <SELECT NAME="DCHK_paytype">!. join('', map {qq!<OPTION VALUE="$_"!.($paytype eq $_ ? ' SELECTED' : '').">$_</OPTION>"} @paytypes). qq!</SELECT><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="">!,
index 8d6e073..470fe71 100644 (file)
@@ -10,10 +10,10 @@ Customer #<B><%= $custnum %></B>
             ? '<I><FONT SIZE="-1">Billing Address</FONT></I><BR>'
             : ''
       %>
-      <%= $first %> <%= $last %><BR>
-      <%= $company ? $company.'<BR>' : '' %>
-      <%= $address1 %><BR>
-      <%= $address2 ? $address2.'<BR>' : '' %>
+      <%= encode_entities($first) %> <%= encode_entities($last) %><BR>
+      <%= $company ? encode_entities($company).'<BR>' : '' %>
+      <%= encode_entities($address1) %><BR>
+      <%= $address2 ? encode_entities($address2).'<BR>' : '' %>
       <%= $city %>, <%= $state %> <%= $zip %><BR>
       <%= $country && $country ne ($countrydefault||'US')
             ? $country.'<BR>'
index b0205ec..0ee8e96 100644 (file)
@@ -1,5 +1,6 @@
 <%= $url = "$selfurl?session=$session_id;action="; ''; %>
-<%= include('header', 'Call usage for '.
+<%= include('header', ($inbound ? 'Received calls' : 'Dialed calls' ) . 
+                       ' for '.
                        Date::Format::time2str('%b&nbsp;%o&nbsp;%Y', $beginning).
                        ' - '.
                        Date::Format::time2str('%b&nbsp;%o&nbsp;%Y', $ending)
index fd5426a..35d1289 100644 (file)
@@ -1,7 +1,20 @@
 <%= $url = "$selfurl?session=$session_id;action=";
-    @svc_acct  = grep { $_->{svcdb} eq 'svc_acct'  } @svcs;
-    @svc_phone = grep { $_->{svcdb} eq 'svc_phone' } @svcs;
-    @svc_port = grep { $_->{svcdb} eq 'svc_port' } @svcs;
+    %by_pkg_label = (); # not used yet, but I'm sure it will be...
+    @svc_acct = ();
+    @svc_phone = ();
+    @svc_port = ();
+
+    foreach (@svcs) {
+      $by_pkg_label{ $_->{pkg_label} } ||= [];
+      push @{ $by_pkg_label{ $_->{pkg_label} } }, $_;
+      if ( $_->{svcdb} eq 'svc_acct' ) {
+        push @svc_acct, $_;
+      } elsif ( $_->{svcdb} eq 'svc_phone' ) {
+        push @svc_phone, $_;
+      } elsif ( $_->{svcdb} eq 'svc_port' ) {
+        push @svc_port, $_;
+      }
+    }
     '';
 %>
 <%= include('header', 'Account usage') %>
 <%= scalar(@svc_acct) ? '</TABLE><BR><BR>' : '' %>
 
 <%= if ( @svc_phone ) {
+      %any = ();
+      for my $dir (qw(outbound inbound)) {
+        $any{$dir} = grep { $_->{$dir} } @svc_phone;
+      }
       $OUT.= '<FONT SIZE="4">Call usage</FONT><BR><BR>
-              <TABLE BGCOLOR="#cccccc">
+              <TABLE BGCOLOR="#cccccc" STYLE="display:inline-block">
                 <TR>
-                  <TH ALIGN="left">Number</TH>'; #"Account" ?
-                                                 #what else?
+                  <TH ALIGN="left">Number</TH>';
+      if ( $any{outbound} ) {
+        $OUT .= '
+                  <TH>Dialed</TH>';
+      }
+      if ( $any{inbound} ) {
+        $OUT .= '
+                  <TH>Received</TH>';
+      }
       $OUT .= '</TR>';
     } else {
       $OUT .= '';
 <%= foreach my $svc_phone ( @svc_phone ) {
       my $link = "${url}view_cdr_details;".
         "svcnum=$svc_phone->{'svcnum'};beginning=0;ending=0";
-  $OUT .= '<TR><TD>';
-    $OUT .= qq!<A HREF="$link">!. $svc_phone->{'label'}. ': '. $svc_phone->{'value'}.'</A>';
-  $OUT .= '</TD></TR>';
+  $OUT .= '<TR><TD>'. $svc_phone->{'label'}. ': '. $svc_phone->{'value'};
+  $OUT .= '</TD>';
+  # usage summary w/ links
+  for my $dir (qw(outbound inbound)) {
+    if ( $dir eq 'inbound' ) {
+      $link .= ';inbound=1';
+    }
+    if ( $svc_phone->{$dir} ) {
+      $OUT .= '<TD ALIGN="right">'.qq!<A HREF="$link">! .
+        sprintf('%d calls (%.0f minutes)',
+          $svc_phone->{$dir}->{'count'},
+          $svc_phone->{$dir}->{'duration'} / 60
+        ) .
+        '</A></TD>';
+    } elsif ( $any{$dir} )  {
+      $OUT .= '<TD></TD>';
+    }
   }
+  $OUT .= '</TR>';
+}
+'';
 %>
 
-<%= scalar(@svc_phone) ? '</TABLE><BR><BR>' : '' %>
+<%= if ( @usage_pools ) {
+  $OUT .= '</TABLE>
+  <TABLE BGCOLOR="#cccccc" STYLE="display: inline-block">
+    <TR><TH COLSPAN=4>Remaining minutes</TH></TR>
+    ';
+  my $any_shared = 0;
+  foreach my $usage (@usage_pools) {
+    # false laziness with the back office side
+    my ($description, $remain, $total, $shared) = @$usage;
+    if ( $shared ) {
+      $any_shared = 1;
+      $description .= '*';
+    }
+    my $ratio = 255 * ($remain/$total);
+    $ratio = 255 if $color > 255;
+    my $color = 
+      sprintf('STYLE="font-weight: bold; color: #%02x%02x00"',
+        255 - $ratio, $ratio);
+    $OUT .=
+    qq!<TR>
+      <TD ALIGN="right">$description</TD>
+      <TD $color ALIGN="right">$remain</TD>
+      <TD $color> / </TD>
+      <TD $color>$total</TD>
+    </TR>!;
+  }
+  if ( $any_shared ) {
+    $OUT .= '<TR STYLE="font-size: 80%; font-style: italic">'.
+            '<TD COLSPAN=4>* shared among all your phone plans</TD></TR>';
+  }
+}
+if ( scalar(@svc_phone) or scalar(@usage_pools) ) {
+  $OUT .= '</TABLE><BR><BR>';
+}
+'';
+%>
 
 <%= if ( @svc_port ) {
       $OUT.= '<FONT SIZE="4">Bandwidth Graphs</FONT><BR><BR>
index 5586e12..71ebfbd 100644 (file)
@@ -77,3 +77,10 @@ PerlHandler HTML::Mason
 SetHandler perl-script
 PerlHandler HTML::Mason
 </DirectoryMatch>
+
+<DirectoryMatch "^%%%FREESIDE_DOCUMENT_ROOT%%%/rt/RTx/Statistics/.*/>
+  <FilesMatch Results.tsv>
+    SetHandler perl-script
+    PerlHandler HTML::Mason
+  </FilesMatch>
+</DirectoryMatch>
index ac8a3a4..7509cf7 100644 (file)
@@ -1,5 +1,5 @@
 <& elements/browse.html,
-     title         => mt('Message catalog'),
+     title         => mt('Translation strings'),
      name_singular => 'string', #mt? no, we need to do it through the quant/PL stuff
      query         => { 'table'     => 'msgcat', 
                         'hashref'   => { 'locale' => $locale, },
index 91238a0..876633a 100755 (executable)
@@ -38,6 +38,21 @@ function part_export_areyousure(href) {
       <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
         <% $part_export->label_html %>
         (<A HREF="<% $p %>edit/part_export.cgi?<% $part_export->exportnum %>">edit</A>&nbsp;|&nbsp;<A HREF="javascript:part_export_areyousure('<% $p %>misc/delete-part_export.cgi?<% $part_export->exportnum %>')">delete</A>)
+%       if ( my @actions = $part_export->actions ) {
+        <P STYLE="position: absolute">
+        Management:
+%         while (@actions) {
+%           my $label = shift @actions;
+%           my $path = shift @actions;
+            <& /elements/popup_link.html,
+              'label'       => $label,
+              'action'      => $fsurl.$path.'?'.$part_export->exportnum,
+              'actionlabel' => $label,
+            &><% @actions ? '&nbsp;|&nbsp;' : '' %>
+%         }
+        </P>
+%       } #if @actions
+
       </TD>
 
       <TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
index 57a4297..bb5bc52 100755 (executable)
@@ -1,6 +1,8 @@
 <% include( 'elements/browse.html',
                  'title'                 => 'Package Definitions',
+                 'menubar'               => \@menubar,
                  'html_init'             => $html_init,
+                 'html_form'             => $html_form,
                  'html_posttotal'        => $html_posttotal,
                  'name'                  => 'package definitions',
                  'disableable'           => 1,
@@ -20,6 +22,9 @@
                  'fields'                => \@fields,
                  'links'                 => \@links,
                  'align'                 => $align,
+                 'link_field'            => 'pkgpart',
+                 'html_init'             => $html_init,
+                 'html_foot'             => $html_foot,
              )
 %>
 <%init>
@@ -33,6 +38,7 @@ my $acl_edit_global = $curuser->access_right($edit_global);
 my $acl_config      = $curuser->access_right('Configuration'); #to edit services
                                                                #and agent types
                                                                #and bulk change
+my $acl_edit_bulk   = $curuser->access_right('Bulk edit package definitions');
 
 die "access denied"
   unless $acl_edit || $acl_edit_global;
@@ -130,9 +136,7 @@ $select = "
 
 ";
 
-my $html_init;
-#unless ( $cgi->param('active') ) {
-  $html_init = qq!
+my $html_init = qq!
     One or more service definitions are grouped together into a package 
     definition and given pricing information.  Customers purchase packages
     rather than purchase services directly.<BR><BR>
@@ -144,7 +148,6 @@ my $html_init;
     </FORM>
     <BR><BR>
   !;
-#}
 
 $cgi->param('dummy', 1);
 
@@ -274,6 +277,18 @@ push @fields, sub {
         : ()
       ),
     ],
+    ( map { my $dst_pkg = $_->dst_pkg;
+            [
+              { data => 'Supplemental: &nbsp;'.
+                        '<A HREF="#'. $dst_pkg->pkgpart . '">' .
+                        $dst_pkg->pkg . '</A>',
+                align=> 'center',
+                colspan => 2,
+              }
+            ]
+          }
+      $part_pkg->supp_part_pkg_link
+    ),
     ( map { 
             my $dst_pkg = $_->dst_pkg;
             [ 
@@ -423,6 +438,10 @@ if ( $taxclasses ) {
   $align .= 'l';
 }
 
+# make a table of report class optionnames =>  the actual 
+my %report_optionname_name = map { 'report_option_'.$_->num, $_->name }
+  qsearch('part_pkg_report_option', { disabled => '' });
+
 push @header, 'Plan options',
               'Services';
               #'Service', 'Quan', 'Primary';
@@ -433,8 +452,18 @@ push @fields,
                     if ( $part_pkg->plan ) {
 
                       my %options = $part_pkg->options;
-
-                      [ map { 
+                      # gather any options that are really report options,
+                      # convert them to their user-friendly names,
+                      # and sort them (I think?)
+                      my @report_options =
+                        sort { $a cmp $b }
+                        map { $report_optionname_name{$_} }
+                        grep { $options{$_}
+                               and exists($report_optionname_name{$_}) }
+                        keys %options;
+
+                      my @rows = (
+                        map { 
                               [
                                 { 'data'  => "$_: ",
                                   'align' => 'right',
@@ -445,11 +474,30 @@ push @fields,
                               ];
                             }
                         grep { $options{$_} =~ /\S/ } 
-                        grep { $_ !~ /^(setup|recur)_fee$/ }
+                        grep { $_ !~ /^(setup|recur)_fee$/ 
+                               and $_ !~ /^report_option_\d+$/ }
                         keys %options
-                      ];
+                      );
+                      if ( @report_options ) {
+                        push @rows,
+                          [ { 'data'  => 'Report classes',
+                              'align' => 'center',
+                              'style' => 'font-weight: bold',
+                              'colspan' => 2
+                            } ];
+                        foreach (@report_options) {
+                          push @rows, [
+                            { 'data'  => $_,
+                              'align' => 'center',
+                              'colspan' => 2
+                            }
+                          ];
+                        } # foreach @report_options
+                      } # if @report_options
+
+                      return \@rows;
 
-                    } else {
+                    } else { # should never happen...
 
                       [ map { [
                                 { 'data'  => uc($_),
@@ -470,6 +518,8 @@ push @fields,
 
               sub {
                     my $part_pkg = shift;
+                    my @part_pkg_usage = sort { $a->priority <=> $b->priority }
+                                         $part_pkg->part_pkg_usage;
 
                     [ 
                       (map {
@@ -512,7 +562,27 @@ push @fields,
                               ]
                             }
                         $part_pkg->svc_part_pkg_link
-                      )
+                      ),
+                      ( scalar(@part_pkg_usage) ? 
+                          [ { data  => 'Usage minutes',
+                              align => 'center',
+                              colspan    => 2,
+                              data_style => 'b',
+                              link  => $p.'browse/part_pkg_usage.html#pkgpart'.
+                                       $part_pkg->pkgpart 
+                            } ]
+                          : ()
+                      ),
+                      ( map {
+                              [ { data  => $_->minutes,
+                                  align => 'right'
+                                },
+                                { data  => $_->description,
+                                  align => 'left'
+                                },
+                              ]
+                            } @part_pkg_usage
+                      ),
                     ];
 
                   };
@@ -527,4 +597,25 @@ $extra_count = ( $count_extra_sql ? ' AND ' : ' WHERE ' ). $extra_count
   if $extra_count;
 my $count_query = "SELECT COUNT(*) FROM part_pkg $count_extra_sql $extra_count";
 
+my $html_form = '';
+my $html_foot = '';
+if ( $acl_edit_bulk ) {
+  # insert a checkbox column
+  push @header, '';
+  push @fields, sub {
+    '<INPUT TYPE="checkbox" NAME="pkgpart" VALUE=' . $_[0]->pkgpart .'>';
+  };
+  push @links, '';
+  $align .= 'c';
+  $html_form = qq!<FORM ACTION="${p}edit/bulk-part_pkg.html" METHOD="POST">!;
+  $html_foot = include('/search/elements/checkbox-foot.html',
+      submit  => 'edit report classes', # for now it's only report classes
+  ) . '</FORM>';
+}
+
+my @menubar;
+# show this if there are any voip_cdr packages defined
+if ( FS::part_pkg->count("plan = 'voip_cdr'") ) {
+  push @menubar, 'Per-package usage minutes' => $p.'browse/part_pkg_usage.html';
+}
 </%init>
diff --git a/httemplate/browse/part_pkg_usage.html b/httemplate/browse/part_pkg_usage.html
new file mode 100644 (file)
index 0000000..209fd3a
--- /dev/null
@@ -0,0 +1,112 @@
+<& /elements/header.html, 'Package usage minutes' &>
+<& /elements/menubar.html, 'Package definitions', $p.'browse/part_pkg.cgi' &>
+<STYLE TYPE="text/css">
+.pkg_head {
+  background-color: #dddddd;
+  font-style: italic;
+}
+.pkg_head > td {
+  border-style: solid;
+  border-radius: 3px;
+  border-color: #555555;
+  border-width: 1px;
+}
+.usage > td {
+  text-align: center;
+}
+.error {
+  color: #ff0000;
+}
+</STYLE>
+<FORM METHOD="POST" ACTION="<%$fsurl%>edit/process/part_pkg_usage.html">
+  <TABLE STYLE="margin-top: 1em">
+    <TR>
+      <TH>Minutes</TH>
+      <TH>Shared</TH>
+      <TH>Rollover</TH>
+      <TH>Description</TH>
+      <TH>Priority</TH>
+%   foreach my $class (@usage_class) {
+      <TH><% $class->classname %></TH>
+%   }
+    </TR>
+
+% my $error = $cgi->param('error');
+% foreach my $part_pkg (@part_pkg) {
+%   my $pkgpart = $part_pkg->pkgpart;
+%   my @part_pkg_usage;
+%   if ( $error ) {
+%     @part_pkg_usage = @{ $error->{$pkgpart} };
+%   } else {
+%     @part_pkg_usage = $part_pkg->part_pkg_usage;
+%     foreach my $usage (@part_pkg_usage) {
+%       foreach ($usage->classnums) {
+%         $usage->set("class$_".'_', 'Y');
+%       }
+%     }
+%   }
+    <TR CLASS="pkg_head" ID="pkgpart<%$pkgpart%>">
+      <TD COLSPAN=<%$n_cols%>><% $part_pkg->pkg_comment %></TD>
+%   # make it easy to enumerate the pkgparts later
+      <INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $pkgpart %>">
+    </TR>
+%   # template row
+    <TR id="pkgpart<%$pkgpart%>_template" CLASS="usage">
+      <TD>
+        <INPUT TYPE="hidden" NAME="pkgusagepart">
+        <INPUT TYPE="text" NAME="minutes" ID="minutes" SIZE=7>
+      </TD>
+%     foreach (qw(shared rollover)) {
+      <TD>
+        <INPUT TYPE="checkbox" NAME="<% $_ %>" ID="<% $_ %>" VALUE="Y">
+      </TD>
+%     }
+      <TD>
+        <INPUT TYPE="text" NAME="description" ID="description" SIZE=20>
+      </TD>
+      <TD>
+        <INPUT TYPE="text" NAME="priority" ID="priority" SIZE=3>
+      </TD>
+%     foreach (@usage_class) {
+%       my $classnum = 'class' . $_->classnum . '_';
+      <TD>
+        <INPUT TYPE="checkbox" NAME="<% $classnum %>" ID="<% $classnum %>" VALUE="Y">
+      </TD>
+%     }
+    </TR>
+    <& /elements/auto-table.html,
+      table         => "pkgpart$pkgpart",
+      template_row  => "pkgpart$pkgpart".'_template',
+      data          => \@part_pkg_usage,
+    &>
+%   }
+  </TABLE>
+  <BR>
+  <INPUT TYPE="submit">
+</FORM>
+<& /elements/footer.html &>
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied"
+  unless $curuser->access_right(
+    ['Edit package definitions', 'Edit global package definitions']
+  );
+
+my @where = ("(plan = 'voip_cdr' OR plan = 'voip_inbound')",
+             "freq != '0'",
+             "disabled IS NULL");
+push @where, FS::part_pkg->curuser_pkgs_sql
+  unless $curuser->access_right('Edit global package definitions');
+my $extra_sql = ' WHERE '.join(' AND ', @where);
+my @part_pkg = qsearch({
+  'table'     => 'part_pkg',
+  'extra_sql' => $extra_sql,
+  'order_by'  => ' ORDER BY pkgpart',
+});
+
+my @usage_class = sort { $a->weight <=> $b->weight } 
+  qsearch('usage_class', { disabled => '' });
+
+my $n_usage_classes = scalar(@usage_class);
+my $n_cols = $n_usage_classes + 5; # minutes, shared, rollover, desc, prio
+</%init>
index a8f4a7c..0d36853 100755 (executable)
@@ -82,6 +82,7 @@ function part_export_areyousure(href) {
 %            }
 %            @dfields ;
 %     my $rowspan = scalar(@fields) || 1;
+%     $rowspan++ if $part_svc->restrict_edit_password;
 %     my $url = "${p}edit/part_svc.cgi?". $part_svc->svcpart;
 %
 %     if ( $bgcolor eq $bgcolor1 ) {
@@ -174,24 +175,32 @@ function part_export_areyousure(href) {
 % my $value = &$formatter($part_svc->part_svc_column($field)->columnvalue);
 % if ( $flag =~ /^[MAH]$/ ) { 
 %   my $select_table = ($flag eq 'H') ? 'hardware_class' : 'inventory_class';
-%   $select_class{$value} ||= 
-%       qsearchs($select_table, { 'classnum' => $value } );
+%   foreach my $classnum ( split(',', $value) ) {
+%     $select_class{$classnum} =
+%       qsearchs($select_table, { 'classnum' => $classnum } );
 % 
-            <% $select_class{$value}
-                  ? $select_class{$value}->classname
-                  : "WARNING: $select_table.classnum $value not found" %>
+      <% $select_class{$classnum}
+            ? $select_class{$classnum}->classname
+            : "WARNING: $select_table.classnum $classnum not found" %><BR>
+%   }
 % } else { 
 
             <% $value %>
-% } 
+% }
 
      </TD>
 %     $n1="</TR><TR>";
-%     }
-%
+%     } #foreach $field
+%   if ( $part_svc->restrict_edit_password ) {
+   <TR>
+     <TD CLASS="grid" BGCOLOR="<% $bgcolor %>" COLSPAN=4 ALIGN="left">
+      <B><% emt('Password editing restricted.') %></B>
+     </TD>
+   </TR>
+%   }
 
   </TR>
-% } 
+% }  #foreach $part_svc
 
 </TABLE>
 </BODY>
index b958894..b0ce467 100644 (file)
@@ -62,8 +62,14 @@ tie my %granularity, 'Tie::IxHash', FS::rate_detail::granularities();
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
 
+my $sub_prefixes = sub {
+  my $region = shift;
+  $region->prefixes .
+  ($region->exact_match ? ' <I>(exact match only)</I>' : '');
+};
+
 my @header     = ( '#',         'Region',  'Country code', 'Prefixes' );
-my @fields     = ( 'regionnum', 'regionname',   'ccode',   'prefixes' );
+my @fields     = ( 'regionnum', 'regionname',   'ccode',   $sub_prefixes );
 my @links      = ( ($link) x 4 );
 my @align      = ( 'right', 'left', 'right', 'left' );
 my @xls_format = ( ({ locked=>1, bg_color=>22 }) x 4 );
index e40b243..05a89c1 100644 (file)
@@ -6,7 +6,7 @@
 
 <P>
 
-Copyright &copy; 2005-2012 Freeside Internet Services, Inc.<BR>
+Copyright &copy; 2005-2013 Freeside Internet Services, Inc.<BR>
 Copyright &copy; 2000-2005 Ivan Kohler<BR>
 Copyright &copy; 1999 Silicon Interactive Software Design<BR>
 All rights reserved<BR>
index 166a3b7..99e911a 100755 (executable)
@@ -9,6 +9,29 @@
 <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-en.js"></SCRIPT>
 <SCRIPT TYPE="text/javascript" SRC="../elements/calendar-setup.js"></SCRIPT>
 
+<SCRIPT TYPE="text/javascript">
+var submit_fields = [];
+function confirm_changes() {
+  var i;
+  var querystring = 'pkgnum=<%$pkgnum%>';
+  var f = document.forms.formname;
+  for(i = 0; i < submit_fields.length; i++) {
+    querystring += ';'
+                + submit_fields[i]
+                + '='
+                + encodeURIComponent(f.elements[submit_fields[i] + '_text'].value);
+  }
+  overlib(
+    OLiframeContent(
+      '<%$p%>/misc/confirm-cust_pkg-edit_dates.html?' + querystring,
+      576, 576, 'confirm_popup'
+    ),
+    CAPTION, 'Package date changes', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', 
+    MIDX, 0, MIDY, 0, DRAGGABLE, BGCOLOR, '#333399', CGCOLOR, '#333399', 
+    TEXTSIZE, 3
+  );
+}
+</SCRIPT>
 <FORM NAME="formname" ACTION="process/REAL_cust_pkg.cgi" METHOD="POST">
 <INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
 
     <TD BGCOLOR="#ffffff"><% $part_pkg->pkg %></TD>
   </TR>
 
+% if ( $cust_pkg->main_pkgnum ) {
+%   my $main_pkg = $cust_pkg->main_pkg;
+  <TR>
+    <TD ALIGN="right">Supplemental to</TD>
+    <TD BGCOLOR="#ffffff">Package #<% $cust_pkg->main_pkgnum%>:&nbsp;\
+    <% $main_pkg->part_pkg->pkg %></TD>
+  </TR>
+
+% }
   <TR>
     <TD ALIGN="right">Custom</TD>
     <TD BGCOLOR="#ffffff"><% $part_pkg->custom %></TD>
@@ -38,7 +70,7 @@
 
   <TR>
     <TD ALIGN="right">Comment</TD>
-    <TD BGCOLOR="#ffffff"><% $part_pkg->comment %></TD>
+    <TD BGCOLOR="#ffffff"><% $part_pkg->comment |h %></TD>
   </TR>
 
   <TR>
 % if ( $cust_pkg->setup && ! $cust_pkg->start_date ) {
   <& .row_display, cust_pkg=>$cust_pkg, column=>'start_date',   label=>'Start' &>
 % } else {
-  <& .row_edit, cust_pkg=>$cust_pkg, column=>'start_date', label=>'Start' &>
+  <& .row_edit, cust_pkg=>$cust_pkg, column=>'start_date', label=>'Start', if_primary=>1 &>
 % }
 
-  <& .row_edit, cust_pkg=>$cust_pkg, column=>'setup',     label=>'Setup' &>
+  <& .row_edit, cust_pkg=>$cust_pkg, column=>'setup',     label=>'Setup', if_primary=>1 &>
   <& .row_edit, cust_pkg=>$cust_pkg, column=>'last_bill', label=>$last_bill_or_renewed &>
   <& .row_edit, cust_pkg=>$cust_pkg, column=>'bill',      label=>$next_bill_or_prepaid_until &>
 %#if ( $cust_pkg->contract_end or $part_pkg->option('contract_end_months',1) ) {
-    <& .row_edit, cust_pkg=>$cust_pkg, column=>'contract_end',label=>'Contract end' &>
+    <& .row_edit, cust_pkg=>$cust_pkg, column=>'contract_end',label=>'Contract end', if_primary=>1 &>
 %#}
   <& .row_display, cust_pkg=>$cust_pkg, column=>'adjourn',  label=>'Adjournment', note=>'(will <b>suspend</b> this package when the date is reached)' &>
   <& .row_display, cust_pkg=>$cust_pkg, column=>'susp',     label=>'Suspension' &>
   $column
   $label
   $note => ''
+  $if_primary => 0
 </%args>
 % my $value = $cust_pkg->get($column);
 % $value = $value ? time2str($format, $value) : "";
-
+%
+% # if_primary for the dates that can't be edited on supplemental packages
+% if ($if_primary and $cust_pkg->main_pkgnum) {
+  <INPUT TYPE="hidden" ID="<%$column%>_text" VALUE="<% $cust_pkg->get($column) %>">
+  <SCRIPT>submit_fields.push('<%$column%>');</SCRIPT>
+  <& .row_display, %ARGS &>
+% } else {
   <TR>
     <TD ALIGN="right"><% $label %> date</TD>
     <TD>
       button:     "<% $column %>_button",
       align:      "BR"
     });
-  </SCRIPT>
 
+    submit_fields.push('<%$column%>');
+
+  </SCRIPT>
+% }
 </%def>
 
 <%def .row_display>
   $column
   $label
   $note => ''
+  $is_primary => 0 #ignored
 </%args>
 % if ( $cust_pkg->get($column) ) { 
     <TR>
 </TABLE>
 
 <BR>
-<INPUT TYPE="submit" VALUE="<% mt('Apply changes') |h %>">
+<INPUT TYPE="button" VALUE="<% mt('Apply changes') |h %>" onclick="confirm_changes()">
 </FORM>
 
 <% include('/elements/footer.html') %>
@@ -160,38 +203,6 @@ if ( $cgi->param('error') ) {
     my @errors = ();
     my %errors = map { $_=>1 } split(',', $cgi->param('error'));
     $cgi->param('error', '');
-
-    if ( $errors{'_bill_areyousure'} ) {
-      if ( $cgi->param('bill') =~ /^([\s\d\/\:\-\(\w\)]*)$/ ) {
-        my $bill = $1;
-        push @errors,
-          "You are attempting to set the next bill date to $bill, which is
-           in the past.  This will charge the customer for the interval
-           from $bill until now.  Are you sure you want to do this? ".
-          '<INPUT TYPE="checkbox" NAME="bill_areyousure" VALUE="1">';
-      }
-    }
-
-    if ( $errors{'_setup_areyousure'} ) {
-      push @errors,
-        "You are attempting to remove the setup date.  This will re-charge the
-         customer for the setup fee.  Are you sure you want to do this? ".
-        '<INPUT TYPE="checkbox" NAME="setup_areyousure" VALUE="1">';
-    }
-
-    if ( $errors{'_setupadd_areyousure'} ) {
-      push @errors,
-        "You are attempting to add a setup date.  This will prevent charging the
-         customer for the setup fee.  Are you sure you want to do this? ".
-        '<INPUT TYPE="checkbox" NAME="setupadd_areyousure" VALUE="1">';
-    }
-
-    if ( $errors{'_start'} ) {
-      push @errors,
-        "You are attempting to add a start date to a package that has already
-         started billing.";
-    }
-
     $error = join('<BR><BR>', @errors );
 
   }
diff --git a/httemplate/edit/bulk-part_pkg.html b/httemplate/edit/bulk-part_pkg.html
new file mode 100644 (file)
index 0000000..a1c6f0c
--- /dev/null
@@ -0,0 +1,74 @@
+<& /elements/header.html, 'Edit package report classes' &>
+%# change that title if we add any other editing controls
+
+%# this should be centralized somewhere
+<STYLE TYPE="text/css">
+.row0 { background-color: #eeeeee; }
+.row1 { background-color: #ffffff; }
+</STYLE>
+
+<FORM ACTION="process/bulk-part_pkg.html" METHOD="POST">
+<DIV>
+The following packages will be changed:<BR>
+% foreach my $pkgpart (sort keys(%part_pkg)) {
+<INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $pkgpart %>">
+<% $part_pkg{$pkgpart}->pkg_comment |h %><BR>
+% }
+</DIV>
+<BR>
+<& /elements/table-grid.html &>\
+<& /elements/tr-justtitle.html, value => mt('Report classes') &>
+% my $row = 0;
+% foreach my $num (sort keys %report_class) {
+  <TR CLASS="row<%$row % 2%>">
+    <TD>
+%   if ( defined $initial_state{$num} ) {
+      <& /elements/checkbox.html,
+            field => 'report_option_'.$num,
+            value => 1,
+            curr_value => $initial_state{$num}
+      &>
+%   } else {
+%     # needs to be a tristate so that you can say "don't change it"
+      <& /elements/checkbox-tristate.html, field => 'report_option_'.$num &>
+%   }
+    </TD>
+    <TD><% $report_class{$num}->name %></TD>
+  </TR>
+%   $row++;
+% }
+</TABLE>
+<BR>
+<INPUT TYPE="submit">
+</FORM>
+<& /elements/footer.html &>
+<%init>
+die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Bulk edit package definitions');
+my @pkgparts = $cgi->param('pkgpart')
+  or die "no package definitions selected";
+
+my %part_pkg = map { $_ => FS::part_pkg->by_key($_) } @pkgparts;
+my %part_pkg_option = map { $_ => { $part_pkg{$_}->options } } @pkgparts;
+my %report_class = map { $_->num => $_ }
+  qsearch('part_pkg_report_option', { disabled => '' });
+
+my %initial_state;
+foreach my $num (keys %report_class) {
+  my $yes = 0;
+  my $no = 0;
+  foreach my $option (values %part_pkg_option) {
+    if ( $option->{"report_option_$num"} ) {
+      $yes = 1;
+    } else {
+      $no = 1;
+    }
+  }
+  if ( $yes and $no ) {
+    $initial_state{$num} = undef;
+  } elsif ( $yes ) {
+    $initial_state{$num} = 1;
+  } elsif ( $no ) {
+    $initial_state{$num} = 0;
+  } # else, uh, you didn't provide any pkgparts
+}
+</%init>
index 3d1cf24..a5ecb69 100644 (file)
   <TH ALIGN="right" COLSPAN=2>Total credit amount: </TD>
   <TH ALIGN="right" ID="total_td"><% $money_char %><% sprintf('%.2f', 0) %></TD>
 </TR>
-<INPUT TYPE="hidden" NAME="amount" ID="total_el" VALUE="0.00">
 
 </table>
 
+<INPUT TYPE="hidden" NAME="amount" ID="total_el" VALUE="0.00">
+
 <table>
 
 <& /elements/tr-select-reason.html,
@@ -244,7 +245,7 @@ function calc_total(what) {
 <%init>
 
 my $curuser = $FS::CurrentUser::CurrentUser;
-die "access denied" unless $curuser->access_right('Post credit');
+die "access denied" unless $curuser->access_right('Credit line items');
 
 #a tiny bit of false laziness w/search/cust_bill_pkg.cgi, but we're pretty
 # specialized and a piece of UI, not a report
index 80b27c2..b90ba66 100755 (executable)
@@ -7,20 +7,32 @@ ACTION="<% $p %>edit/process/cust_location.cgi" METHOD=POST>
 <INPUT TYPE="hidden" NAME="locationnum" VALUE="<% $locationnum %>">
 
 <% ntable('#cccccc') %>
-<% include('/elements/location.html',
-            'object'        => $cust_location,
-            'no_asterisks'  => 1,
-            ) %>
+<& /elements/location.html,
+  'object'              => $cust_location,
+  'no_asterisks'        => 1,
+  # these are service locations, so they need all this stuff
+  'enable_coords'       => 1,
+  'enable_district'     => 1,
+  'enable_censustract'  => 1,
+&>
+<& /elements/standardize_locations.html,
+            'form'          => 'EditLocationForm',
+            'callback'      => 'document.EditLocationForm.submit();',
+&>
 </TABLE>
 
 <BR>
 <SCRIPT TYPE="text/javascript">
-function areyousure() {
-  return confirm('Modify this service location?');
+function go() {
+% if ( FS::Conf->new->config('address_standardize_method') ) {
+  standardize_locations();
+% } else {
+  confirm('Modify this service location?') &&
+    document.EditLocationForm.submit();
+% }
 }
 </SCRIPT>
-<INPUT TYPE="submit" VALUE="Submit" onclick="return areyousure()">
-
+<INPUT TYPE="button" NAME="submitButton" VALUE="Submit" onclick="go()">
 </FORM>
 </BODY>
 </HTML>
index be00213..2908848 100755 (executable)
@@ -48,7 +48,7 @@
   <TD STYLE="width:650px">
 %#; padding-right:2px; vertical-align:top">
     <FONT CLASS="fsinnerbox-title"><% mt('Billing address') |h %></FONT>
-    <TABLE CLASS="fsinnerbox">
+    <TABLE CLASS="fsinnerbox" WIDTH="100%">
     <& cust_main/before_bill_location.html, $cust_main &>
     <& /elements/location.html,
         object => $cust_main->bill_location,
@@ -62,7 +62,6 @@
 <TR><TD STYLE="height:40px"></TD></TR>
 <TR>
   <TD STYLE="width:650px">
-%#; padding-left:2px; vertical-align:top">
     <FONT CLASS="fsinnerbox-title"><% mt('Service address') |h %></FONT>
     <INPUT TYPE="checkbox" 
            NAME="same"
            VALUE="Y"
            <% $has_ship_address ? '' : 'CHECKED' %>
     ><% mt('same as billing address') |h %>
-    <TABLE CLASS="fsinnerbox" ID="table_ship_location">
-    <& /elements/location.html,
-        object => $cust_main->ship_location,
-        prefix => 'ship_',
-        enable_censustract => 1,
-        enable_district => 1,
-        enable_coords => 1,
-    &>
-    </TABLE>
-    <TABLE CLASS="fsinnerbox" ID="table_ship_location_blank"
-    STYLE="display:none">
-    <TR><TD></TD></TR>
-    </TABLE>
+    <DIV CLASS="fsinnerbox">
+      <TABLE ID="table_ship_location" WIDTH="100%">
+      <& /elements/location.html,
+          object => $cust_main->ship_location,
+          prefix => 'ship_',
+          enable_censustract => 1,
+          enable_district => 1,
+          enable_coords => 1,
+      &>
+      </TABLE>
+    </DIV>
   </TD>
 </TR></TABLE>
 
@@ -94,19 +91,14 @@ function samechanged(what) {
 %#  document.getElementById('table_ship_location').style.visibility = 
 %#    what.checked ? 'hidden' : 'visible';
   var t1 = document.getElementById('table_ship_location');
-  var t2 = document.getElementById('table_ship_location_blank');
   if ( what.checked ) {
-    t2.style.width  = t1.clientWidth  + 'px';
-    t2.style.height = t1.clientHeight + 'px';
-    t1.style.display = 'none';
-    t2.style.display = '';
+    t1.style.visibility = 'hidden';
   }
   else {
-    t2.style.display = 'none';
-    t1.style.display = '';
+    t1.style.visibility = 'visible'
   }
 }
-samechanged(document.getElementById('same'));
+//samechanged(document.getElementById('same'));
 </SCRIPT>
 
 <BR>
@@ -285,7 +277,8 @@ if ( $cgi->param('error') ) {
   my( $query ) = $cgi->keywords;
   $query =~ /^(\d+)$/;
   $custnum=$1;
-  $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+  $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+    or die "custnum $custnum not found";
   if ( $cust_main->dbdef_table->column('paycvv')
        && length($cust_main->paycvv)             ) {
     my $paycvv = $cust_main->paycvv;
index 2925ca8..5a66f0a 100644 (file)
 
     <TR><TD>&nbsp;</TD></TR>
 
+%   my $curuser = $FS::CurrentUser::CurrentUser;
 %   my @exempt_groups = grep /\S/, $conf->config('tax-cust_exempt-groups');
-
 %   if (    $conf->exists('cust_class-tax_exempt')
 %        || $conf->exists('tax-cust_exempt-groups-require_individual_nums')
+%        || ! $curuser->access_right('Edit customer tax exemptions')
 %      )
 %   {
 
 
 %   }
 
-%   foreach my $exempt_group ( @exempt_groups ) {
-%     my $cust_main_exemption = $cust_main->tax_exemption($exempt_group);
-%     #escape $exempt_group for NAME etc.
-%     my $checked = ($cust_main_exemption || $cgi->param("tax_$exempt_group"));
-      <TR>
-        <TD>&nbsp;&nbsp;<INPUT TYPE="checkbox" NAME="tax_<% $exempt_group %>" ID="tax_<% $exempt_group %>" VALUE="Y" <% $checked ? 'CHECKED' : '' %> onChange="tax_changed(this)"> Tax Exempt (<% $exempt_group %> taxes)</TD>
-        <TD> - Exemption number <INPUT TYPE="text" NAME="tax_<% $exempt_group %>_num" ID="tax_<% $exempt_group %>_num" VALUE="<% $cgi->param("tax_$exempt_group".'_num') || ( $cust_main_exemption ? $cust_main_exemption->exempt_number : '' ) |h %>" <% $checked ? '' : 'DISABLED' %>></TD>
-      </TR>
+%   if ( $curuser->access_right('Edit customer tax exemptions') ) {
+%     foreach my $exempt_group ( @exempt_groups ) {
+%       my $cust_main_exemption = $cust_main->tax_exemption($exempt_group);
+%       #escape $exempt_group for NAME etc.
+%       my $checked = ($cust_main_exemption || $cgi->param("tax_$exempt_group"));
+        <TR>
+          <TD>&nbsp;&nbsp;<INPUT TYPE="checkbox" NAME="tax_<% $exempt_group %>" ID="tax_<% $exempt_group %>" VALUE="Y" <% $checked ? 'CHECKED' : '' %> onChange="tax_changed(this)"> Tax Exempt (<% $exempt_group %> taxes)</TD>
+          <TD> - Exemption number <INPUT TYPE="text" NAME="tax_<% $exempt_group %>_num" ID="tax_<% $exempt_group %>_num" VALUE="<% $cgi->param("tax_$exempt_group".'_num') || ( $cust_main_exemption ? $cust_main_exemption->exempt_number : '' ) |h %>" <% $checked ? '' : 'DISABLED' %>></TD>
+        </TR>
+%     }
 %   }
 
 % unless ( $conf->exists('emailinvoiceonly') ) {
         <% $conf->exists('cust_main-require_invoicing_list_email', $agentnum) 
             ? $r : '' %>Email address(es)
       </TD>
-      <TD WIDTH="408"><INPUT TYPE="text" NAME="invoicing_list" VALUE="<% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) %>"></TD>
+      <TD WIDTH="408"><INPUT TYPE="text" NAME="invoicing_list" VALUE="<% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) %>">
+      <INPUT TYPE="checkbox" NAME="message_noemail" VALUE="Y" <%
+        ( $cust_main->message_noemail eq 'Y' )
+          ? 'CHECKED'
+          : ''
+        %>> <% emt('Do not send notices') %>
+      </TD>
     </TR>
 % }
 
index 1cfa52d..0de6d9d 100644 (file)
@@ -70,8 +70,8 @@ function copy_payby_fields() {
 
 <& /elements/standardize_locations.js,
   'callback' => 'submit_continue();',
-  'main_prefix' => 'bill_',
-  'no_company' => 1,
+  'billship' => 1,
+  'with_census' => 1, # no with_firm, apparently
 &>
 
 function copyelement(from, to) {
diff --git a/httemplate/edit/cust_main/choose_tax_location.html b/httemplate/edit/cust_main/choose_tax_location.html
deleted file mode 100644 (file)
index ac475c5..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-<FORM NAME="choosegeocodeform">
-<CENTER><BR><B>Choose tax location</B><BR><BR>
-<P>the geocode is:<% $header %></P>
-<P STYLE="<% $style %>"><% $header %></P>
-
-<SELECT NAME='geocodes' ID='geocodes' STYLE="<% $style %>">
-% foreach my $location (@cust_tax_location) {
-%   my %value = ( zip => $zip5,
-%                 map { $_ => $location->$_ }
-%                   qw ( city state geocode )
-%               );
-%   map { $value{$_} = $location{$_} } qw ( city state )
-%     if $location{country} eq 'CA';
-%
-%   my $value = encode_entities(objToJson({ %value })
-%                              );
-%   my $content = '';
-%   $content .= $location->$_. '&nbsp;' x ( $max{$_} - length($location->$_) )
-%     foreach qw( city county state );
-%   $content .=   $location->cityflag eq 'I' ? 'Y' : 'N' ;
-%   my $selected = '' ;
-%   if ($geocode && $location->geocode eq $geocode) {
-%     $selected = 'SELECTED';
-%   }
-  <OPTION VALUE="<% $value %>" STYLE="<% $style %>" <% $selected %>><% $content %>
-% }
-</SELECT><BR><BR>
-
-<TABLE><TR>
-  <TD> <BUTTON TYPE="button" onClick="set_geocode(document.getElementById('geocodes'));"><IMG SRC="<%$p%>images/tick.png" ALT=""> Set location </BUTTON></TD>
-  <TD><BUTTON TYPE="button" onClick="document.CustomerForm.submitButton.disabled=false; parent.cClick();"><IMG SRC="<%$p%>images/cross.png" ALT=""> Cancel submission </BUTTON></TD>
-</TR>
-</TABLE>
-
-</CENTER>
-</FORM>
-<%init>
-
-my $conf = new FS::Conf;
-
-my %location = ();
-
-($location{data_vendor}) = $cgi->param('data_vendor') =~ /^([-\w]+)$/;
-($location{city})        = $cgi->param('city')        =~ /^([\w ]+)$/;
-($location{state})       = $cgi->param('state')       =~ /^(\w+)$/;
-($location{zip})         = $cgi->param('zip')         =~ /^([-\w ]+)$/;
-($location{country})     = $cgi->param('country')     =~ /^([\w ]+)$/;
-
-my($geocode)             = $cgi->param('geocode')     =~ /^([\w]+)$/;
-
-my($zip5, $zip4) = split('-', $location{zip});
-
-#only support US & CA
-my $hashref = { 'data_vendor' => $location{data_vendor} };
-$hashref->{zip} = $location{country} eq 'CA' ? substr($zip5,0,1) : $zip5,
-
-my @keys = keys(%$hashref);
-my @cust_tax_location = ();
-until ( @cust_tax_location ) {
-  @cust_tax_location = qsearch({ table    => 'cust_tax_location',
-                                 hashref  =>  $hashref,
-                                 order_by =>  'LIMIT 50',
-                              });
-  last unless scalar(@keys);
-  delete $hashref->{ shift @keys };
-} 
-
-my %max = ( city => 4, county => 6, state => 5);
-foreach my $location (@cust_tax_location) {
-  foreach ( qw( city county state ) ) {
-    my $length = length($location->$_);
-    $max{$_} = ($length > $max{$_}) ? $length : $max{$_};
-  }
-}
-foreach ( qw( city county state ) ) {
-  $max{$_} = $location{$_} if $location{$_} > $max{$_};
-  $max{$_}++;
-}
-
-my $header = '&nbsp;&nbsp;';
-$header .= $_. '&nbsp;' x ( $max{lc($_)} - length($_) )
-  foreach qw( City County State );
-$header .=   "In city?";
-
-my $style = "font-family:monospace;";
-
-</%init>
index cfed8e4..b7e86ba 100644 (file)
       document.getElementById('contacts_div').style.display = 'none';
     }
   }
+
+  var ship_locked_agents = <% encode_json(\%ship_locked_agents) %>;
+  var ship_fields = ['address1', 'city', 'state', 'zip', 'country', 
+    'latitude', 'longitude', 'district'];
+  function agent_changed(what) {
+    var agentnum = what.value;
+    var f = what.form;
+    if ( ship_locked_agents[agentnum] ) {
+%     # For this agent, the service location (except address2)
+%     # should be locked to the agent's location.
+%     # Set the ship_ fields to those values (just for display) and
+%     # then disable them.
+      for(var x in ship_locked_agents[agentnum]) {
+        f['ship_'+x].value = ship_locked_agents[agentnum][x];
+        f['ship_'+x].disabled = true;
+      }
+      f['same'].checked = false;
+      f['same'].disabled = true;
+    } else {
+%     # Unlock the ship_ location fields.  If they were previously
+%     # disabled, then they contain some agent's address, which is 
+%     # no longer meaningful.  So set them back to the customer's 
+%     # current location.
+      for(var i=0; i<ship_fields.length; i++) {
+        x = ship_fields[i];
+        if ( f['ship_'+x].disabled )  {
+          f['ship_'+x].value  = f['old_ship_'+x].value;
+        }
+        f['ship_'+x].disabled = false;
+      }
+      f['same'].disabled = false;
+    }
+    samechanged(f['same']);
+  }
+  window.onload = function() {
+    agent_changed(document.getElementById('agentnum'));
+  }
 </SCRIPT>
 
 % foreach my $field ($cust_main->virtual_fields) {
 %   $cust_main->agentnum($agentnum);
 
     <INPUT TYPE="hidden" NAME="lock_agentnum" VALUE="<% $agentnum %>">
-    <INPUT TYPE="hidden" NAME="agentnum"      VALUE="<% $agentnum %>">
+    <INPUT TYPE="hidden" NAME="agentnum"      ID="agentnum" 
+      VALUE="<% $agentnum %>">
     <TR>
       <TD ALIGN="right"><% mt('Agent') |h %></TD>
       <TD CLASS="fsdisabled"><% $cust_main->agent->agent |h %></TD>
     </TR>
+
 % } else {
 
   <& /elements/tr-select-agent.html, 
                 'empty_label'   => emt('Select agent'),
                 'disable_empty' => ( $cust_main->agentnum ? 1 : 0 ),
                 'viewall_right' => emt('None'), 
+                'onchange'      => 'agent_changed(this)',
   &>
 
 % }
@@ -201,4 +241,17 @@ my $curuser = $FS::CurrentUser::CurrentUser;
 
 my $r = qq!<font color="#ff0000">*</font>&nbsp;!;
 
+# which agents lock the service address, if any
+my %ship_locked_agents;
+foreach (qsearch('agent',{})) {
+  my $agentnum = $_->agentnum;
+  next unless $conf->exists('agent-ship_address', $_->agentnum);
+  my $cust_main = $_->agent_cust_main or next;
+  my $agent_ship_location = $cust_main->ship_location;
+  $ship_locked_agents{$agentnum} = +{
+    map { $_ => $agent_ship_location->$_ }
+    qw(address1 city state zip country latitude longitude district)
+  };
+}
+
 </%init>
index dd1ed33..88e9254 100755 (executable)
@@ -7,7 +7,6 @@
 <INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
 
 %#current packages
-%my @cust_pkg = qsearch('cust_pkg', { 'custnum' => $custnum, 'cancel' => '' } );
 %if (@cust_pkg) {
 
   Current packages - select to remove (services are moved to a new package below)
     </TR>
   <BR><BR>
 %
-%
-%  foreach ( sort {     $all_pkg{ $a->getfield('pkgpart') }
-%                   cmp $all_pkg{ $b->getfield('pkgpart') }
-%                 }
-%                 @cust_pkg
-%          )
-%  {
+%  foreach ( @main_pkgs ) {
 %    my($pkgnum,$pkgpart)=( $_->getfield('pkgnum'), $_->getfield('pkgpart') );
 %    my $checked = $remove_pkg{$pkgnum} ? ' CHECKED' : '';
 %
       <TD ALIGN="right"><% $pkgnum %>:</TD>
       <TD><% $all_pkg{$pkgpart} %> - <% $all_comment{$pkgpart} %></TD>
     </TR>
+%   foreach my $supp_pkg ( @{ $supp_pkgs_of{$pkgnum} } ) {
+    <TR>
+      <TD></TD>
+      <TD></TD>
+      <TD>+ <% $all_pkg{$supp_pkg->pkgpart} %> - <% $all_comment{$supp_pkg->pkgpart} %></TD>
+    </TR>
+%   }
 % } 
 
 
@@ -147,4 +147,24 @@ if ( $cgi->param('error') ) {
 
 my $p1 = popurl(1);
 
+my @cust_pkg = qsearch('cust_pkg', { 'custnum' => $custnum, 'cancel' => '' } );
+my @main_pkgs;
+my %supp_pkgs_of; # main pkgnum => arrayref of cust_pkgs
+
+
+foreach my $cust_pkg
+  ( sort { $all_pkg{ $a->pkgpart } cmp $all_pkg{ $b->getfield('pkgpart') } }
+    @cust_pkg
+  )
+  # XXX does not properly handle recursive supplemental links
+{
+  if ( my $main_pkgnum = $cust_pkg->main_pkgnum ) {
+    $supp_pkgs_of{$main_pkgnum} ||= [];
+    push @{ $supp_pkgs_of{$main_pkgnum} }, $cust_pkg;
+  } else {
+    push @main_pkgs, $cust_pkg;
+    $supp_pkgs_of{$cust_pkg->pkgnum} ||= [];
+  }
+}
+
 </%init>
index 009ed5c..5e10706 100644 (file)
@@ -28,7 +28,7 @@
 
   <TR>
     <TD ALIGN="right">Comment</TD>
-    <TD BGCOLOR="#ffffff"><% $part_pkg->comment %></TD>
+    <TD BGCOLOR="#ffffff"><% $part_pkg->comment |h %></TD>
   </TR>
 
   <TR>
diff --git a/httemplate/edit/cust_pkg_quantity.html b/httemplate/edit/cust_pkg_quantity.html
new file mode 100755 (executable)
index 0000000..ec47ed6
--- /dev/null
@@ -0,0 +1,49 @@
+<& /elements/header-popup.html, "Change Quantity" &>
+<& /elements/error.html &>
+
+<FORM ACTION="<% $p %>edit/process/cust_pkg_quantity.html" METHOD=POST>
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
+<& /elements/table-grid.html, 'bgcolor' => '#cccccc', 'cellpadding' => 2 &>
+
+  <TR>
+    <TH ALIGN="right">Current package&nbsp;</TH>
+    <TD CLASS="grid">
+      <% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><B><% $part_pkg->pkg |h %></B> - <% $part_pkg->comment |h %>
+    </TD>
+  </TR>
+
+<& /elements/tr-input-text.html,
+    'field'       => 'quantity',
+    'curr_value'  => $cust_pkg->quantity,
+    'label'       => emt('Quantity')
+&>
+
+</TABLE>
+
+<BR>
+<INPUT NAME="submit" TYPE="submit" VALUE="Change">
+
+</FORM>
+</BODY>
+</HTML>
+
+<%init>
+
+#some false laziness w/misc/change_pkg.cgi
+
+my $conf = new FS::Conf;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('Change customer package');
+
+my $pkgnum = scalar($cgi->param('pkgnum'));
+$pkgnum =~ /^(\d+)$/ or die "illegal pkgnum $pkgnum";
+$pkgnum = $1;
+
+my $cust_pkg = FS::cust_pkg->by_key($pkgnum) or die "unknown pkgnum $pkgnum";
+
+my $part_pkg = $cust_pkg->part_pkg;
+
+</%init>
index 656d5eb..df42e63 100755 (executable)
     </TD>
   </TR>
 % } 
-
+% if ( $cust_pay->processor ) {
     <TR>
       <TD ALIGN="right">Processor</TD>
       <TD BGCOLOR="#ffffff"><% $cust_pay->processor %></TD>
     </TR>
-% if ( length($auth) ) { 
+% if ( length($cust_pay->auth) ) { 
 
       <TR>
         <TD ALIGN="right">Authorization</TD>
         <TD BGCOLOR="#ffffff"><% $cust_pay->order_number %></TD>
       </TR>
 % } 
-% }  #if $cust_pay
+% } # if ($cust_pay->processor)
 
   </TABLE>
-% } 
+% }  #if $cust_pay
 
 
 <BR>Refund
index a24f238..3e6bd5b 100644 (file)
@@ -329,6 +329,7 @@ Example:
 %     qw( country ),                                       #select-country
 %     qw( width height ),                                  #htmlarea
 %     qw( alt_format ),                                    #select-cust_location
+%     qw( classnum ),                                   # select-inventory_item
 %   ;
 %
 %   #select-table
diff --git a/httemplate/edit/elements/part_svc_column.html b/httemplate/edit/elements/part_svc_column.html
new file mode 100644 (file)
index 0000000..d03c49d
--- /dev/null
@@ -0,0 +1,303 @@
+<%doc>
+To be called from part_svc.cgi.
+<& elements/part_svc_column.html, 
+    'svc_acct',
+    # options...
+    'part_svc'  => $part_svc, # the existing part_svc to edit
+    'clone'     => 0,         # or a svcpart to clone from
+&>
+
+</%doc>
+<%once>
+# the semantics of this could be better
+
+# all of these conditions are when NOT to allow that flag choice
+# don't allow the 'inventory' flags (M, A) to be chosen for 
+# fields that aren't free-text
+my $inv_sub = sub { $_[0]->{disable_inventory} || $_[0]->{type} ne 'text' };
+tie my %flag, 'Tie::IxHash',
+  ''  => { 'desc' => 'No default', 'condition' => sub { 0 } },
+  'D' => { 'desc' => 'Default', 
+           'condition' =>
+             sub { $_[0]->{disable_default } }
+         },
+  'F' => { 'desc' => 'Fixed (unchangeable)',
+           'condition' =>
+             sub { $_[0]->{disable_fixed} },
+         },
+  'S' => { 'desc' => 'Selectable Choice',
+           'condition' =>
+             sub { $_[0]->{disable_select} },
+         },
+  'M' => { 'desc' => 'Manual selection from inventory',
+           'condition' => $inv_sub,
+         },
+  'A' => { 'desc' => 'Automatically fill in from inventory',
+           'condition' => $inv_sub,
+         },
+  'H' => { 'desc' => 'Select from hardware class',
+           'condition' => sub { $_[0]->{type} ne 'select-hardware' },
+         },
+  'X' => { 'desc' => 'Excluded',
+           'condition' => sub { 1 }, # obsolete
+         },
+;
+
+# the semantics of this could be much better
+sub flag_condition {
+  my $f = shift;
+  not &{ $flag{$f}->{'condition'} }(@_);
+}
+
+my %communigate_fields = (
+  'svc_acct'        => { map { $_=>1 }
+                            qw( file_quota file_maxnum file_maxsize
+                                password_selfchange password_recover
+                              ),
+                            grep /^cgp_/, fields('svc_acct')
+  },
+  'svc_domain'      => { map { $_=>1 }
+                            qw( max_accounts trailer parent_svcnum ),
+                            grep /^(cgp|acct_def)_/, fields('svc_domain')
+  },
+);
+</%once>
+<INPUT TYPE="hidden" NAME="svcdb" VALUE="<% $svcdb %>">
+<BR><BR>
+<& /elements/table.html &>
+  <TR><TH COLSPAN=<% $columns %>>Exports</TH></TR>
+  <TR>
+% # exports
+% foreach my $part_export (@part_export) {
+    <TD>
+      <INPUT TYPE="checkbox" \
+             NAME="exportnum<% $part_export->exportnum %>" \
+             VALUE=1 \
+             <% $has_export_svc{$part_export->exportnum} ? 'CHECKED' : '' %>>
+      <% $part_export->label_html %>
+    </TD>
+%   $count++;
+%   if ( $count % $columns == 0 ) {
+  </TR>
+  <TR>
+%   }
+% }
+  </TR>
+</TABLE><BR><BR>
+For the selected table, you can give fields default or fixed (unchangeable)
+values, or select an inventory class to manually or automatically fill in 
+that field.
+<& /elements/table-grid.html, cellpadding => 4 &>
+  <TR>
+    <TH BGCOLOR="#cccccc">Field</TH>
+    <TH BGCOLOR="#cccccc">Label</TH>
+    <TH BGCOLOR="#cccccc" COLSPAN=2>Modifier</TH>
+  </TR>
+% $part_svc->set('svcpart' => $opt{'clone'}) if $opt{'clone'}; # for now
+% my $i = 0;
+% foreach my $field (@fields) {
+%   my $def = shift @defs;
+%   my $part_svc_column = $part_svc->part_svc_column($field);
+%   my $flag = $part_svc_column->columnflag;
+%   my $formatter = $def->{'format'} || sub { shift };
+%   my $value = &{$formatter}($part_svc_column->columnvalue);
+  <TR CLASS="row<%$i%>">
+    <TD ROWSPAN=2 CLASS="grid" ALIGN="right">
+      <% $def->{'label'} || $field %>
+    </TD>
+    <TD ROWSPAN=2 CLASS="grid">
+      <INPUT NAME="<% $svcdb %>__<% $field %>_label"
+             STYLE="text-align: right"
+             VALUE="<% $part_svc_column->columnlabel || $def->{'label'} |h %>">
+    </TD>
+
+    <TD ROWSPAN=1 CLASS="grid">
+%   # flag selection
+%   if ( $def->{'type'} eq 'disabled' ) {
+%     $flag = '';
+      No default
+%   } else {
+%     my $name = $svcdb.'__'.$field.'_flag';
+      <SELECT NAME="<%$name%>"
+              ID="<%$name%>"
+              STYLE="width:100%"
+              onchange="flag_changed(this)">
+%     foreach my $f (keys %flag) {
+%       if ( flag_condition($f, $def, $svcdb, $field) ) {
+          <OPTION VALUE="<%$f%>"<% $flag eq $f ? ' SELECTED' : ''%>>
+            <% $flag{$f}->{desc} %>
+          </OPTION>
+%       }
+%     }
+      </SELECT>
+%   } # if $def->{'type'} eq 'disabled'
+    </TD>
+    <TD CLASS="grid">
+%   # value entry/selection
+%   my $name = $svcdb.'__'.$field;
+%   # These are all MANDATORY SELECT types.  Regardless of the flag value,
+%   # there will never be a text input (either in svc_* or in part_svc) for
+%   # these fields.
+%   if ( $def->{'type'} eq 'checkbox' ) {
+      <& /elements/checkbox.html,
+          'field'       => $name,
+          'curr_value'  => $value,
+          'value'       => 'Y' &>
+%
+%   } elsif ( $def->{'type'} eq 'select' ) {
+%
+%     if ( $def->{'select_table'} ) {
+      <& /elements/select-table.html,
+          'field'       => $name,
+          'id'          => $name.'_select',
+          'table'       => $def->{'select_table'},
+          'name_col'    => $def->{'select_label'},
+          'value_col'   => $def->{'select_key'},
+          'order_by'    => dbdef->table($def->{'select_table'})->primary_key,
+          'multiple'    => $def->{'multiple'},
+          'disable_empty' => 1,
+          'curr_value'  => $value,
+      &>
+%     } else {
+%       my (@options, %labels);
+%       if ( $def->{'select_list'} ) {
+%         @options = @{ $def->{'select_list'} };
+%         @labels{@options} = @options;
+%       } elsif ( $def->{'select_hash'} ) {
+%         if ( ref($def->{'select_hash'}) eq 'ARRAY' ) {
+%           tie my %hash, 'Tie::IxHash', @{ $def->{'select_hash'} };
+%           $def->{'select_hash'} = \%hash;
+%         }
+%         @options = keys( %{ $def->{'select_hash'} } );
+%         %labels = %{ $def->{'select_hash'} };
+%       }
+      <& /elements/select.html,
+          'field'       => $name,
+          'id'          => $name.'_select',
+          'options'     => \@options,
+          'labels'      => \%labels,
+          'multiple'    => $def->{'multiple'},
+          'curr_value'  => $value,
+      &>
+%     }
+%   } elsif ( $def->{'type'} =~ /select-(.*?).html/ ) {
+      <& '/elements/'.$def->{'type'},
+          'field'       => $name,
+          'id'          => $name.'_select',
+          'multiple'    => $def->{'multiple'},
+          'curr_value'  => $value,
+      &>
+%   } elsif ( $def->{'type'} eq 'communigate_pro-accessmodes' ) {
+      <& /elements/communigate_pro-accessmodes.html,
+          'element_name_prefix' => $name.'_',
+          'curr_value'  => $value,
+      &>
+%   } elsif ( $def->{'type'} eq 'textarea' ) {
+%   # special cases
+      <TEXTAREA NAME="<%$name%>"><% $value |h %></TEXTAREA>
+%   } elsif ( $def->{'type'} eq 'disabled' ) {
+      <INPUT TYPE="hidden" NAME="<%$name%>" VALUE="">
+%   } else {
+%     # the normal case: a text input, and a _select which is an inventory
+%     # or hardware class
+      <INPUT TYPE="text"
+             NAME="<%$name%>"
+             ID="<%$name%>" 
+             VALUE="<%$value%>">
+%     # inventory class selection
+      <& /elements/select-table.html,
+          'field'       => $name.'_classnum',
+          'id'          => $name.'_select',
+          'table'       => 'inventory_class',
+          'name_col'    => 'classname',
+          'curr_value'  => $value,
+          'empty_label' => 'Select inventory class',
+          'multiple'    => 1,
+      &>
+%   }
+    </TD>
+  </TR>
+  <TR CLASS="row<%$i%>">
+    <TD COLSPAN=2 CLASS="def_info">
+%   if ( $def->{def_info} ) {
+      (<% $def->{def_info} %>)
+    </TD>
+  </TR>
+%   }
+% $i = 1-$i;
+% } # foreach my $field
+%
+% # special case: svc_acct password edit ACL
+% if ( $svcdb eq 'svc_acct' ) {
+%   push @fields, 'restrict_edit_password';
+  <TR>
+    <TD COLSPAN=3 ALIGN="right">
+      <% emt('Require "Provision" access right to edit password') %>
+    </TD>
+    <TD>
+      <INPUT TYPE="checkbox" NAME="restrict_edit_password" VALUE="Y" \
+      <% $part_svc->restrict_edit_password ? 'CHECKED' : '' %>>
+    </TD>
+  </TR>
+% }
+</TABLE>
+<& /elements/progress-init.html,
+  $svcdb, #form name
+  [ # form fields to send
+    qw(svc svcpart classnum selfservice_access disabled preserve exportnum),
+    @fields
+  ],
+  'process/part_svc.cgi',   # target
+  $p.'browse/part_svc.cgi', # redirect landing
+  $svcdb, #key
+&>
+% $svcpart = '' if $opt{clone};
+<BR>
+<INPUT NAME="submit"
+       TYPE="button"
+       VALUE="<% emt($svcpart ? 'Apply changes' : 'Add service') %>"
+       onclick="fixup_submit('<%$svcdb%>')"
+>
+<%init>
+my $svcdb = shift;
+my %opt = @_;
+my $columns = 3;
+my $count = 0;
+my $communigate = 0;
+my $conf = FS::Conf->new;
+
+my $part_svc = $opt{'part_svc'} || FS::part_svc->new;
+
+my @part_export;
+my $export_info = FS::part_export::export_info($svcdb);
+foreach (keys %{ $export_info }) {
+  push @part_export, qsearch('part_export', { exporttype => $_ });
+}
+$communigate = scalar(grep {$_->exporttype =~ /^communigate/} @part_export);
+
+my $svcpart = $opt{'clone'} || $part_svc->svcpart;
+my %has_export_svc;
+if ( $svcpart ) {
+  foreach (qsearch('export_svc', { svcpart => $svcpart })) {
+    $has_export_svc{$_->exportnum} = 1;
+  }
+}
+
+my @fields;
+if ( defined( dbdef->table($svcdb) ) ) { # when is it ever not defined?
+  @fields = grep {
+    $_ ne 'svcnum'
+      and ( $communigate || ! $communigate_fields{$svcdb}->{$_} )
+      and ( !FS::part_svc->svc_table_fields($svcdb)->{$_}->{disable_part_svc_column}
+            || $part_svc->part_svc_column($_)->columnflag )
+  } fields($svcdb);
+}
+if ( $svcdb eq 'svc_acct'
+      or ( $svcdb eq 'svc_broadband' and $conf->exists('svc_broadband-radius') )
+   )
+{
+  push @fields, 'usergroup';
+}
+
+my @defs = map { FS::part_svc->svc_table_fields($svcdb)->{$_} } @fields;
+</%init>
index 0d9d36c..d46d1cb 100644 (file)
                    } elsif ( $flag eq 'A' ) {
                      $f->{'type'} = 'hidden';
                    } elsif ( $flag eq 'M' ) {
+                     $f->{'type'} = 'select-inventory_item';
                      $f->{'empty_label'} = 'Select inventory item';
-                     $f->{'type'}        = 'select-table';
-                     $f->{'table'}       = 'inventory_item';
-                     $f->{'name_col'}    = 'item'; 
-                     $f->{'value_col'}   = 'item'; 
-                     $f->{'agent_virt'}  = 1;
-                     $f->{'agent_null'}  = 1;
-                     $f->{'hashref'}     = {
-                                            'classnum'=>$columndef->columnvalue,
-                                            #'svcnum'  => '',
-                                           };
-                     $f->{'extra_sql'}   = 'AND ( svcnum IS NULL ';
-                     $f->{'extra_sql'}  .= ' OR svcnum = '. $object->svcnum
-                       if $object->svcnum;
-                     $f->{'extra_sql'}  .= ' ) ';
+                     $f->{'extra_sql'} = 'WHERE ( svcnum IS NULL ' .
+                        ($object->svcnum && ' OR svcnum = '.$object->svcnum) .
+                        ')';
+                     $f->{'classnum'} = $columndef->columnvalue;
                      $f->{'disable_empty'} = $object->svcnum ? 1 : 0;
-                     if ( $f->{'field'} eq 'mac_addr' ) {
-                       $f->{'compare_sub'} = sub {
-                         my($a, $b) = @_;
-                         $a =~ s/[-: ]//g;
-                         $b =~ s/[-: ]//g;
-                         lc($a) eq lc($b);
-                       };
-                     }
                    } elsif ( $flag eq 'H' ) {
                      $f->{'type'}        = 'select-hardware_type';
                      $f->{'hashref'}     = {
index 4dd253b..2897cf3 100644 (file)
@@ -2,6 +2,34 @@
 
 <% include('/elements/error.html') %>
 
+<SCRIPT TYPE="text/javascript">
+  function svc_machine_changed (what, layer) {
+    if ( what.checked ) {
+      var machine = document.getElementById(layer + "_machine");
+      var part_export_machine = 
+        document.getElementById(layer + "_part_export_machine");
+      if ( what.value == 'Y' ) {
+        machine.disabled = true;
+        part_export_machine.disabled = false;
+      } else if ( what.value == 'N' ) {
+        machine.disabled = false;
+        part_export_machine.disabled = true;
+      }
+    }
+  }
+
+  function part_export_machine_changed (what, layer) {
+    var select_default = document.getElementById(layer + '_default_machine');
+    var selected = select_default.value;
+    select_default.options.length = 0;
+    var choices = what.value.split("\n");
+    for (var i = 0; i < choices.length; i++) {
+      select_default.options[i] = new Option(choices[i]);
+    }
+    select_default.value = selected;
+  }
+
+</SCRIPT>
 <FORM NAME="dummy">
 <INPUT TYPE="hidden" NAME="exportnum" VALUE="<% $part_export->exportnum %>">
 
@@ -58,7 +86,6 @@ my $widget = new HTML::Widgets::SelectLayers(
   'form_name'      => 'dummy',
   'form_action'    => 'process/part_export.cgi',
   'form_text'      => [qw( exportnum exportname )],
-#  'form_checkbox'  => [qw()],
   'html_between'    => "</TD></TR></TABLE>\n",
   'layer_callback'  => sub {
     my $layer = shift;
@@ -87,7 +114,8 @@ my $widget = new HTML::Widgets::SelectLayers(
         if ( $exports->{$layer}{svc_machine} ) {
           my( $N_CHK, $Y_CHK) = ( 'CHECKED', '' );
           my( $machine_DISABLED, $pem_DISABLED) = ( '', 'DISABLED' );
-          my $part_export_machine = '';
+          my @part_export_machine;
+          my $default_machine = '';
           if ( $cgi->param('svc_machine') eq 'Y'
                  || $machine eq '_SVC_MACHINE'
              )
@@ -97,38 +125,43 @@ my $widget = new HTML::Widgets::SelectLayers(
             $machine_DISABLED = 'DISABLED';
             $pem_DISABLED = '';
             $machine = '';
-            $part_export_machine =
-              $cgi->param('part_export_machine')
-              || join "\n",
+            @part_export_machine = $cgi->param('part_export_machine');
+            if (!@part_export_machine) {
+              @part_export_machine = 
                    map $_->machine,
                      grep ! $_->disabled,
                        $part_export->part_export_machine;
+            }
+            $default_machine =
+              $cgi->param('default_machine_name')
+              || $part_export->default_export_machine;
           }
-          my $oc = qq(onChange="${layer}_svc_machine_changed(this)");
+          my $oc = qq(onChange="svc_machine_changed(this, '$layer')");
           $html .= qq[
             <INPUT TYPE="radio" NAME="svc_machine" VALUE="N" $N_CHK $oc>
             <INPUT TYPE="text" NAME="machine" ID="${layer}_machine" VALUE="$machine" $machine_DISABLED>
             <BR>
             <INPUT TYPE="radio" NAME="svc_machine" VALUE="Y" $Y_CHK $oc>
-            Selected in each customer service from these choices
-            <TEXTAREA NAME="part_export_machine" ID="${layer}_part_export_machine" $pem_DISABLED>$part_export_machine</TEXTAREA>
-
-            <SCRIPT TYPE="text/javascript">
-              function ${layer}_svc_machine_changed (what) {
-                if ( what.checked ) {
-                  var machine = document.getElementById("${layer}_machine");
-                  var part_export_machine = document.getElementById("${layer}_part_export_machine");
-                  if ( what.value == 'Y' ) {
-                    machine.disabled = true;
-                    part_export_machine.disabled = false;
-                  } else if ( what.value == 'N' ) {
-                    machine.disabled = false;
-                    part_export_machine.disabled = true;
-                  }
-                }
-              }
-            </SCRIPT>
+            <DIV STYLE="display:inline-block; vertical-align: top; text-align: right">
+              Selected in each customer service from these choices:
+              <TEXTAREA STYLE="vertical-align: top" NAME="part_export_machine"
+                ID="${layer}_part_export_machine"
+                onchange="part_export_machine_changed(this, '$layer')"
+                $pem_DISABLED>] .
+                
+                join("\n", @part_export_machine) .
+                
+                qq[</TEXTAREA>
+              <BR>
+              Default: 
+              <SELECT NAME="default_machine_name" ID="${layer}_default_machine">
           ];
+          foreach (@part_export_machine) {
+            $_ = encode_entities($_); # oh noes, XSS
+            my $sel = ($default_machine eq $_) ? ' SELECTED' : '';
+            $html .= qq!<OPTION VALUE="$_"$sel>$_</OPTION>\n!;
+          }
+          $html .= '</DIV></SELECT>'
         } else {
           $html .= qq(<INPUT TYPE="text" NAME="machine" VALUE="$machine">).
                      '<INPUT TYPE="hidden" NAME="svc_machine" VALUE=N">';
index c3f4f88..fadde35 100755 (executable)
@@ -28,7 +28,8 @@
 
               'labels' => { 
                             'pkgpart'          => 'Package Definition',
-                            'pkg'              => 'Package (customer-visible)',
+                            'pkg'              => 'Package',
+                            %locale_field_labels,
                             'comment'          => 'Comment (customer-hidden)',
                             'classnum'         => 'Package class',
                             'addon_classnum'   => 'Restrict additional orders to package class',
@@ -53,6 +54,7 @@
                             'discountnum'      => 'Offer discounts for longer terms',
                             'bill_dst_pkgpart' => 'Include line item(s) from package',
                             'svc_dst_pkgpart'  => 'Include services of package',
+                            'supp_dst_pkgpart' => 'Include complete package',
                             'report_option'    => 'Report classes',
                             'fcc_ds0s'         => 'Voice-grade equivalents',
                             'fcc_voip_class'   => 'Category',
@@ -79,6 +81,7 @@
                                 size      => 40, #32
                                 maxlength => 50,
                               },
+                              #@locale_fields,
                               {field=>'comment',  type=>'text', size=>40 }, #32
                               { field         => 'agentnum',
                                 type          => 'select-agent',
                             },
 
                             { 'type'    => 'tablebreak-tr-title',
+                              'value'   => 'Supplemental packages',
+                              'colspan' => '4',
+                            },
+                            { 'field'       => 'supp_dst_pkgpart',
+                              'type'        => 'select-part_pkg',
+                              'm2_label'    => 'Include complete package',
+                              'm2m_method'  => 'supp_part_pkg_link',
+                              'm2m_dstcol'  => 'dst_pkgpart',
+                              'm2_error_callback' =>
+                                &{$m2_error_callback_maker}('supp'),
+                            },
+
+                            { 'type'    => 'tablebreak-tr-title',
                               'value'   => 'Pricing add-ons',
                               'colspan' => 4,
                             },
@@ -323,6 +339,22 @@ my $agent_clone_extra_sql =
 my $conf = new FS::Conf;
 my $taxproducts = $conf->exists('enable_taxproducts');
 
+my @locales = grep { ! /^en_/i } $conf->config('available-locales'); #should filter from the default locale lang instead of en_
+my %locale_labels =  map {
+  ( $_ => 'Package -- '. FS::Locales->description($_) )
+} @locales;
+@locales = 
+  sort { $locale_labels{$a} cmp $locale_labels{$b} }
+    @locales;
+
+my $n = 0;
+my %locale_field_labels = (
+  map {
+        ( 'pkgpartmsgnum'. $n++. '_pkg' => $locale_labels{$_} );
+      }
+    @locales
+);
+
 my $sth = dbh->prepare("SELECT COUNT(*) FROM part_pkg_report_option".
                        "  WHERE disabled IS NULL OR disabled = ''  ")
   or die dbh->errstr;
@@ -354,6 +386,42 @@ my $recur_show_zero_disabled = 1;
 
 my $pkgpart = '';
 
+my $splice_locale_fields = sub {
+  my( $fields, $pkey_value_callback, $pkg_value_callback ) = @_;
+
+  my $n = 0;
+  my @locale_fields = (
+    map { 
+          my $pkey_value= $pkey_value_callback ? &$pkey_value_callback($_) : '';
+          my $pkg_value = $pkg_value_callback
+                            ? $pkg_value_callback eq 'cgiparam'
+                                ? $cgi->param('pkgpartmsgnum'. $n. '_pkg')
+                                : &$pkg_value_callback($_)
+                            : '';
+          (
+            { field     => 'pkgpartmsgnum'. $n,
+              type      => 'hidden',
+              value     => $pkey_value,
+            },
+            { field     => 'pkgpartmsgnum'. $n. '_locale',
+              type      => 'hidden',
+              value     => $_,
+            },
+            { field     => 'pkgpartmsgnum'. $n++. '_pkg',
+              type      => 'text',
+              size      => 40,
+              #maxlength => 50,
+              value     => $pkg_value,
+            },
+          );
+  
+        }
+      @locales
+  );
+  splice(@$fields, 7, 0, @locale_fields); #XXX 7 is arbitrary above
+
+};
+
 my $error_callback = sub {
   my($cgi, $object, $fields, $opt ) = @_;
 
@@ -394,6 +462,16 @@ my $error_callback = sub {
 
   $pkgpart = $object->pkgpart;
 
+  &$splice_locale_fields(
+    $fields,
+    sub {
+          my $locale = shift;
+          my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
+          $part_pkg_msgcat ? $part_pkg_msgcat->pkgpartmsgnum : '';
+        },
+    'cgiparam'
+  );
+
 };
 
 my $new_hashref_callback = sub { { 'plan' => 'flat' }; };
@@ -459,6 +537,20 @@ my $edit_callback = sub {
 
   $pkgpart = $object->pkgpart;
 
+  &$splice_locale_fields(
+    $fields,
+    sub {
+          my $locale = shift;
+          my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
+          $part_pkg_msgcat ? $part_pkg_msgcat->pkgpartmsgnum : '';
+        },
+    sub {
+          my $locale = shift;
+          my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
+          $part_pkg_msgcat ? $part_pkg_msgcat->pkg : '';
+        }
+  );
+
 };
 
 my $new_callback = sub {
@@ -473,6 +565,8 @@ my $new_callback = sub {
 
   $options{'suspend_bill'}=1 if $conf->exists('part_pkg-default_suspend_bill');
 
+  &$splice_locale_fields($fields, '', '');
+
 };
 
 my $clone_callback = sub {
@@ -506,6 +600,16 @@ my $clone_callback = sub {
     foreach (qw( setup_fee recur_fee disable_line_item_date_ranges ));
 
   $recur_disabled = $object->freq ? 0 : 1;
+
+  &$splice_locale_fields(
+    $fields,
+    '',
+    sub {
+      my $locale = shift;
+      my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
+      $part_pkg_msgcat ? $part_pkg_msgcat->pkg : '';
+    }
+  );
 };
 
 my $discount_error_callback = sub {
index 007c246..58c237e 100755 (executable)
-<& /elements/header.html, "$action Service Definition",
-           menubar('View all service definitions' => "${p}browse/part_svc.cgi"),
+<& /elements/header.html, "$action Service Definition" &>
+<& /elements/menubar.html,
+  'View all service definitions' => "${p}browse/part_svc.cgi"
            #" onLoad=\"visualize()\""
 &>
 
 <& /elements/init_overlib.html &>
 
-<BR>
+<BR><BR>
+
+<STYLE TYPE="text/css">
+.disabled {
+  background-color: #dddddd;
+}
+.hidden {
+  display: none;
+}
+.enabled {
+  background-color: #ffffff;
+}
+.row0 TD {
+  background-color: #eeeeee;
+}
+.row1 TD {
+  background-color: #ffffff;
+}
+.def_info {
+  text-align: center;
+  padding: 0px;
+  border-top: none;
+  font-size: smaller;
+  font-style: italic;
+}
+</STYLE>
+<SCRIPT TYPE="text/javascript">
+function fixup_submit(layer) {
+  document.forms[layer].submit.disabled = true;
+  fixup(document.forms[layer]);
+  window[layer+'process'].call();
+}
+
+function flag_changed(obj) {
+  var newflag = obj.value;
+  var a = obj.name.match(/(.*)__(.*)_flag/);
+  var layer = a[1];
+  var field = a[2];
+  var input = document.getElementById(layer + '__' + field);
+  // for fields that have both 'input' and 'select', 'select' is 'select from
+  // inventory class'.
+  var select = document.getElementById(layer + '__' + field + '_select');
+  if (newflag == "" || newflag == "X") { // disable
+    if ( input ) {
+      input.disabled = true;
+      input.className = 'disabled';
+    }
+    if ( select ) {
+      select.disabled = true;
+      select.className = 'hidden';
+    }
+  } else if ( newflag == 'D' || newflag == 'F' || newflag == 'S' ) {
+    if ( input ) {
+      // enable text box, disable inventory select
+      input.disabled = false;
+      input.className = 'enabled';
+      if ( select ) {
+        select.disabled = false;
+        select.className = 'hidden';
+      }
+    } else if ( select ) {
+      // enable select
+      select.disabled = false;
+      select.className = 'enabled';
+      if ( newflag == 'S' || select.getAttribute('should_be_multiple') ) {
+        select.multiple = true;
+      } else {
+        select.multiple = false;
+      }
+    }
+  } else if ( newflag == 'M' || newflag == 'A' || newflag == 'H' ) {
+    // these all require a class selection
+    if ( select ) {
+      select.disabled = false;
+      select.className = 'enabled';
+      if ( input ) {
+        input.disabled = false;
+        input.className = 'hidden';
+      }
+    }
+  }
+}
+
+window.onload = function() {
+  var selects = document.getElementsByTagName('SELECT');
+  for(i = 0; i < selects.length; i++) {
+    var obj = selects[i];
+    if ( obj.multiple ) {
+      obj.setAttribute('should_be_multiple', true);
+    }
+  }
+  for(i = 0; i < selects.length; i++) {
+    var obj = selects[i];
+    if ( obj.name.match(/_flag$/) ) {
+      flag_changed(obj);
+    }
+  }
+};
+
+</SCRIPT>
 
 <FORM NAME="dummy">
 
 
 <BR>
 
-% my %vfields;
-%  #code duplication w/ edit/part_svc.cgi, should move this hash to part_svc.pm
-%  # and generalize the subs
-%  # condition sub is tested to see whether to disable display of this choice
-%  # params: ( $def, $layer, $field )  (see SUB below)
-%  my $inv_sub = sub {
-%                      $_[0]->{disable_inventory}
-%                        || $_[0]->{'type'} ne 'text'
-%                    };
-%  tie my %flag, 'Tie::IxHash',
-%    ''  => { 'desc' => 'No default', },
-%    'D' => { 'desc' => 'Default',
-%             'condition' =>
-%               sub { $_[0]->{disable_default} }, 
-%           },
-%    'F' => { 'desc' => 'Fixed (unchangeable)',
-%             'condition' =>
-%               sub { $_[0]->{disable_fixed} }, 
-%           },
-%    'S' => { 'desc' => 'Selectable Choice',
-%             'condition' =>
-%               sub { !ref($_[0]) || $_[0]->{disable_select} }, 
-%           },
-%    'M' => { 'desc' => 'Manual selection from inventory',
-%             'condition' => $inv_sub,
-%           },
-%    'A' => { 'desc' => 'Automatically fill in from inventory',
-%             'condition' => $inv_sub,
-%           },
-%    'H' => { 'desc' => 'Select from hardware class',
-%             'condition' => sub { $_[0]->{type} ne 'select-hardware' },
-%           },
-%    'X' => { 'desc' => 'Excluded',
-%             'condition' =>
-%               sub { ! $vfields{$_[1]}->{$_[2]} },
-%
-%           },
-%  ;
-%  
-%  my @dbs = $hashref->{svcdb}
-%             ? ( $hashref->{svcdb} )
-%             : FS::part_svc->svc_tables();
-%
-%  my $help = '';
-%  unless ( $hashref->{svcpart} ) {
-%    $help = '&nbsp;'.
-%            include('/elements/popup_link.html',
-%                      'action' => $p.'docs/part_svc-table.html',
-%                      'label'  => 'help',
-%                      'actionlabel' => 'Service table help',
-%                      'width'       => 763,
-%                      #'height'      => 400,
-%                    );
-%  }
-%
-%  tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } grep dbdef->table($_), @dbs;
-%  my $widget = new HTML::Widgets::SelectLayers(
-%    #'selected_layer' => $p_svcdb,
-%    'selected_layer' => $hashref->{svcdb} || 'svc_acct',
-%    'options'        => \%svcdb,
-%    'form_name'      => 'dummy',
-%    #'form_action'    => 'process/part_svc.cgi',
-%    'form_action'    => 'part_svc.cgi', #self
-%    'form_elements'  => [qw( svc svcpart classnum selfservice_access
-%                             disabled preserve
-%                        )],
-%    'html_between'   => $help,
-%    'layer_callback' => sub {
-%      my $layer = shift;
-%      
-%      my $html = qq!<INPUT TYPE="hidden" NAME="svcdb" VALUE="$layer">!;
-%
-%      #$html .= $svcdb_info;
-%
-%      my $columns = 3;
-%      my $count = 0;
-%      my $communigate = 0;
-%      my @part_export =
-%        map { qsearch( 'part_export', {exporttype => $_ } ) }
-%          keys %{FS::part_export::export_info($layer)};
-%      $html .= '<BR><BR>'. include('/elements/table.html') . 
-%               "<TR><TH COLSPAN=$columns>Exports</TH></TR><TR>";
-%      foreach my $part_export ( @part_export ) {
-%        $communigate++ if $part_export->exporttype =~ /^communigate/;
-%        $html .= '<TD><INPUT TYPE="checkbox"'.
-%                 ' NAME="exportnum'. $part_export->exportnum. '"  VALUE="1" ';
-%        $html .= 'CHECKED'
-%          if ( $clone || $part_svc->svcpart ) #null svcpart search causing error
-%              && qsearchs( 'export_svc', {
-%                                   exportnum => $part_export->exportnum,
-%                                   svcpart   => $clone || $part_svc->svcpart });
-%        $html .= '>'. $part_export->label_html. '</TD>';
-%        $count++;
-%        $html .= '</TR><TR>' unless $count % $columns;
-%      }
-%      $html .= '</TR></TABLE><BR><BR>'. $mod_info;
-%
-%      $html .= include('/elements/table-grid.html', 'cellpadding' => 4 ).
-%               '<TR>'.
-%                 '<TH CLASS="grid" BGCOLOR="#cccccc">Field</TH>'.
-%                 '<TH CLASS="grid" BGCOLOR="#cccccc">Label</TH>'.
-%                 '<TH CLASS="grid" BGCOLOR="#cccccc" COLSPAN=2>Modifier</TH>'.
-%               '</TR>';
-%
-%      my $bgcolor1 = '#eeeeee';
-%      my $bgcolor2 = '#ffffff';
-%      my $bgcolor;
-%
-%      #yucky kludge
-%      my @fields = ();
-%      if ( defined( dbdef->table($layer) ) ) {
-%        @fields = grep {
-%            $_ ne 'svcnum'
-%            && ( $communigate || !$communigate_fields{$layer}->{$_} )
-%            && ( !FS::part_svc->svc_table_fields($layer)
-%                   ->{$_}->{disable_part_svc_column}
-%                 || $part_svc->part_svc_column($_)->columnflag
-%               )
-%        } fields($layer);
-%      }
-%      push @fields, 'usergroup' 
-%        if $layer eq 'svc_acct'
-%          or ( $layer eq 'svc_broadband' and 
-%               $conf->exists('svc_broadband-radius') ); # double kludge
-%               # (but we do want to check the config, right?)
-%      $part_svc->svcpart($clone) if $clone; #haha, undone below
-%
-%
-%      foreach my $field (@fields) {
-%
-%        #a few lines of false laziness w/browse/part_svc.cgi
-%        my $def = FS::part_svc->svc_table_fields($layer)->{$field};
-%        my $def_info  = $def->{'def_info'};
-%        my $formatter = $def->{'format'} || sub { shift };
-%
-%        my $part_svc_column = $part_svc->part_svc_column($field);
-%        my $label = $part_svc_column->columnlabel || $def->{'label'};
-%        my $value = &$formatter($part_svc_column->columnvalue);
-%        my $flag  = $part_svc_column->columnflag;
-%
-%        if ( $bgcolor eq $bgcolor1 ) {
-%          $bgcolor = $bgcolor2;
-%        } else {
-%          $bgcolor = $bgcolor1;
-%        }
-%        
-%        $html .= qq!<TR><TD ROWSPAN=2 CLASS="grid" BGCOLOR="$bgcolor" ALIGN="right">!.
-%                 ( $def->{'label'} || $field ).
-%                 "</TD>";
-%
-%        $html .= qq!<TD ROWSPAN=2 CLASS="grid" BGCOLOR="$bgcolor"><INPUT NAME="${layer}__${field}_label" VALUE="!. encode_entities($label). '" STYLE="text-align:right"></TD>';
-%
-%        $flag = '' if $def->{type} eq 'disabled';
-%
-%        $html .= qq!<TD CLASS="grid" BGCOLOR="$bgcolor">!;
-%
-%        if ( $def->{type} eq 'disabled' ) {
-%        
-%          $html .= 'No default';
-%
-%        } else {
-%
-%          $html .= qq!<SELECT NAME="${layer}__${field}_flag"!.
-%                      qq! onChange="${layer}__${field}_flag_changed(this)">!;
-%
-%          foreach my $f ( keys %flag ) {
-%
-%            # need to template-ize more httemplate/edit/svc_* first
-%            next if $f eq 'M' and $layer !~ /^svc_(broadband|external|phone|dish)$/;
-%
-%            #here is where the SUB from above is called, to skip some choices
-%            next if $flag{$f}->{condition}
-%                 && &{ $flag{$f}->{condition} }( $def, $layer, $field );
-%
-%            $html .= qq!<OPTION VALUE="$f"!.
-%                     ' SELECTED'x($flag eq $f ).
-%                     '>'. $flag{$f}->{desc};
-%
-%          }
-%
-%          $html .= '</SELECT>';
-%
-%          $html .= join("\n",
-%            '<SCRIPT>',
-%            "  function ${layer}__${field}_flag_changed(what) {",
-%            '    var f = what.options[what.selectedIndex].value;',
-%            '    if ( f == "" || f == "X" ) { //disable',
-%            "      what.form.${layer}__${field}.disabled = true;".
-%            "      what.form.${layer}__${field}.style.backgroundColor = '#dddddd';".
-%            "      if ( what.form.${layer}__${field}_classnum ) {".
-%            "        what.form.${layer}__${field}_classnum.disabled = true;".
-%            "        what.form.${layer}__${field}_classnum.style.backgroundColor = '#dddddd';".
-%            "      }".
-%            '    } else if ( f == "D" || f == "F" || f =="S" ) { //enable, text box',
-%            "      what.form.${layer}__${field}.disabled = false;".
-%            "      what.form.${layer}__${field}.style.backgroundColor = '#ffffff';".
-%            "      if ( f == 'S' || '${field}' == 'usergroup' ) {". # kludge
-%            "        what.form.${layer}__${field}.multiple = true;".
-%            "      } else {".
-%            "        what.form.${layer}__${field}.multiple = false;".
-%            "      }".
-%            "      what.form.${layer}__${field}.style.display = '';".
-%            "      if ( what.form.${layer}__${field}_classnum ) {".
-%            "        what.form.${layer}__${field}_classnum.disabled = false;".
-%            "        what.form.${layer}__${field}_classnum.style.backgroundColor = '#ffffff';".
-%            "        what.form.${layer}__${field}_classnum.style.display = 'none';".
-%            "      }".
-%            '    } else if ( f == "M" || f == "A" || f == "H" ) { '.
-%                   '//enable, inventory',
-%            "      what.form.${layer}__${field}.disabled = false;".
-%            "      what.form.${layer}__${field}.style.backgroundColor = '#ffffff';".
-%            "      what.form.${layer}__${field}.style.display = 'none';".
-%            "      if ( what.form.${layer}__${field}_classnum ) {".
-%            "        what.form.${layer}__${field}_classnum.disabled = false;".
-%            "        what.form.${layer}__${field}_classnum.style.backgroundColor = '#ffffff';".
-%            "        what.form.${layer}__${field}_classnum.style.display = '';".
-%            "      }".
-%            '    }',
-%            '  }',
-%            '</SCRIPT>',
-%          );
-%
-%        }
-%
-%        $html .= qq!</TD><TD CLASS="grid" BGCOLOR="$bgcolor">!;
-%
-%        my $disabled = $flag ? ''
-%                             : 'DISABLED STYLE="background-color: #dddddd"';
-%        my $nodisplay = ' STYLE="display:none"';
-%
-%        if ( !$def->{type} || $def->{type} eq 'text' ) {
-%
-%          my $is_inv = ( $flag =~ /^[MA]$/ );
-%
-%          $html .=
-%            qq!<INPUT TYPE="text" NAME="${layer}__${field}" VALUE="$value" !.
-%            $disabled.
-%            ( $is_inv ? $nodisplay : $disabled ).
-%            '>';
-%
-%          $html .= include('/elements/select-table.html',
-%                             'element_name' => "${layer}__${field}_classnum",
-%                             'id'           => "${layer}__${field}_classnum",
-%                             'element_etc'  => ( $is_inv
-%                                                   ? $disabled
-%                                                   : $nodisplay
-%                                               ),
-%                             'table'        => 'inventory_class',
-%                             'name_col'     => 'classname',
-%                             'value'        => $value,
-%                             'empty_label'  => 'Select inventory class',
-%                          );
-%
-%        } elsif ( $def->{type} eq 'checkbox' ) {
-%
-%          $html .= include('/elements/checkbox.html',
-%                             'field'      => $layer.'__'.$field,
-%                             'curr_value' => $value,
-%                             'value'      => 'Y',
-%                          );
-%
-%        } elsif ( $def->{type} eq 'select' ) {
-%
-%          $html .= qq!<SELECT NAME="${layer}__${field}" $disabled!;
-%          $html .= ' MULTIPLE' if $flag eq 'S';
-%          $html .= '>';
-%          $html .= '<OPTION> </OPTION>' unless $value;
-%          if ( $def->{select_table} ) {
-%            foreach my $record ( qsearch( $def->{select_table}, {} ) ) {
-%              my $rvalue = $record->getfield($def->{select_key});
-%              my $select_label = $def->{select_label};
-%              $html .= qq!<OPTION VALUE="$rvalue"!.
-%                  (grep(/^$rvalue$/, split(',',$value)) ? ' SELECTED>' : '>' ).
-%                  $record->$select_label(). '</OPTION>';
-%            } #next $record
-%          } elsif ( $def->{select_list} ) {
-%            foreach my $item ( @{$def->{select_list}} ) {
-%              $html .= qq!<OPTION VALUE="$item"!.
-%                    (grep(/^$item$/, split(',',$value)) ? ' SELECTED>' : '>' ).
-%                    $item. '</OPTION>';
-%            } #next $item
-%          } elsif ( $def->{select_hash} ) {
-%            if ( ref($def->{select_hash}) eq 'ARRAY' ) {
-%              tie my %hash, 'Tie::IxHash', @{ $def->{select_hash} };
-%              $def->{select_hash} = \%hash;
-%            }
-%            foreach my $key ( keys %{$def->{select_hash}} ) {
-%              $html .= qq!<OPTION VALUE="$key"!.
-%                    (grep(/^$key$/, split(',',$value)) ? ' SELECTED>' : '>' ).
-%                    $def->{select_hash}{$key}. '</OPTION>';
-%            } #next $key
-%          } #endif
-%          $html .= '</SELECT>';
-%
-%        } elsif ( $def->{type} eq 'textarea' ) {
-%
-%          $html .=
-%            qq!<TEXTAREA NAME="${layer}__${field}">!. encode_entities($value).
-%            '</TEXTAREA>';
-%
-%        } elsif ( $def->{type} =~ /select-(.*?).html/ ) {
-%
-%          $html .= include("/elements/".$def->{type},
-%                             'curr_value'   => $value,
-%                             'element_name' => "${layer}__${field}",
-%                             'element_etc'  => $disabled,
-%                             'multiple'     => ($def->{multiple} ||
-%                                                $flag eq 'S'),
-%                                 # allow the table def to force 'multiple'
-%                          );
-%
-%        } elsif ( $def->{type} eq 'communigate_pro-accessmodes' ) {
-%
-%          $html .= include('/elements/communigate_pro-accessmodes.html',
-%                             'element_name_prefix' => "${layer}__${field}_",
-%                             'curr_value'          => $value,
-%                             #doesn't work#'element_etc'  => $disabled,
-%                          );
-%
-%        } elsif ( $def->{type} eq 'select-hardware' ) {
-%
-%          $html .= qq!<INPUT TYPE="text" NAME="${layer}__${field}" $disabled>!;
-%          $html .= include('/elements/select-hardware_class.html',
-%                             'curr_value'    => $value,
-%                             'element_name'  => "${layer}__${field}_classnum",
-%                             'id'            => "${layer}__${field}_classnum",
-%                             'element_etc'   => $flag ne 'H' && $nodisplay,
-%                             'empty_label'   => 'Select hardware class',
-%                          );
-%
-%        } elsif ( $def->{type} eq 'disabled' ) {
-%
-%          $html .=
-%            qq!<INPUT TYPE="hidden" NAME="${layer}__${field}" VALUE="">!;
-%
-%        } else {
-%
-%          $html .= '<font color="#ff0000">unknown type '. $def->{type};
-%
-%        }
-%
-%        $html .= "</TD></TR>\n";
-
-%        $def_info = "($def_info)" if $def_info;
-%        $html .=
-%          qq!<TR>!.
-%          qq!  <TD COLSPAN=2 BGCOLOR="$bgcolor" ALIGN="center" !.
-%          qq!      STYLE="padding:0; border-top: none">!.
-%          qq!    <FONT SIZE="-1"><I>$def_info</I></FONT>!.
-%          qq!  </TD>!.
-%          qq!</TR>\n!;
-%
-%      } #foreach my $field (@fields) {
-%
-%      $part_svc->svcpart('') if $clone; #undone
-%      $html .= "</TABLE>";
-%
-%      $html .= include('/elements/progress-init.html',
-%                         $layer, #form name
-%                         [ qw(svc svcpart classnum selfservice_access
-%                              disabled preserve
-%                              exportnum),
-%                           @fields ],
-%                         'process/part_svc.cgi',
-%                         $p.'browse/part_svc.cgi',
-%                         $layer,
-%                      );
-%      $html .= '<BR><INPUT NAME="submit" TYPE="button" VALUE="'.
-%               ($hashref->{svcpart} ? 'Apply changes' : 'Add service'). '" '.
-%               ' onClick="document.'. "$layer.submit.disabled=true; ".
-%               "fixup(document.$layer); $layer". 'process();">';
-%
-%      #$html .= '<BR><INPUT TYPE="submit" VALUE="'.
-%      #         ($hashref->{svcpart} ? 'Apply changes' : 'Add service'). '">';
-%
-%      $html;
-%
-%    },
-%  );
-
 <BR>
 Table <% $widget->html %>
 
@@ -465,28 +185,43 @@ my $action = $part_svc->svcpart ? 'Edit' : 'Add';
 my $hashref = $part_svc->hashref;
 #   my $p_svcdb = $part_svc->svcdb || 'svc_acct';
 
-my %communigate_fields = (
-  'svc_acct'        => { map { $_=>1 }
-                           qw( file_quota file_maxnum file_maxsize
-                               password_selfchange password_recover
-                             ),
-                           grep /^cgp_/, fields('svc_acct')
-                       },
-  'svc_domain'      => { map { $_=>1 }
-                           qw( max_accounts trailer parent_svcnum ),
-                           grep /^(cgp|acct_def)_/, fields('svc_domain')
-                       },
-  #'svc_forward'     => { map { $_=>1 } qw( ) },
-  #'svc_mailinglist' => { map { $_=>1 } qw( ) },
-  #'svc_cert'        => { map { $_=>1 } qw( ) },
-);
 
-my $mod_info = '
-For the selected table, you can give fields default or fixed (unchangable)
-values, or select an inventory class to manually or automatically fill in
-that field.
-';
+my @dbs = $hashref->{svcdb}
+           ? ( $hashref->{svcdb} )
+           : FS::part_svc->svc_tables();
+
+my $help = '';
+unless ( $hashref->{svcpart} ) {
+  $help = '&nbsp;'.
+          include('/elements/popup_link.html',
+                    'action' => $p.'docs/part_svc-table.html',
+                    'label'  => 'help',
+                    'actionlabel' => 'Service table help',
+                    'width'       => 763,
+                    #'height'      => 400,
+                  );
+}
 
+tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } grep dbdef->table($_), @dbs;
+my $widget = new HTML::Widgets::SelectLayers(
+  #'selected_layer' => $p_svcdb,
+  'selected_layer' => $hashref->{svcdb} || 'svc_acct',
+  'options'        => \%svcdb,
+  'form_name'      => 'dummy',
+  #'form_action'    => 'process/part_svc.cgi',
+  'form_action'    => 'part_svc.cgi', #self
+  'form_elements'  => [qw( svc svcpart classnum selfservice_access
+                           disabled preserve
+                      )],
+  'html_between'   => $help,
+  'layer_callback' => sub {
+    include('elements/part_svc_column.html',
+              shift,
+              'part_svc' => $part_svc,
+              'clone' => $clone
+    )
+  }
+);
 </%init>
 
 
index 5712560..2cf34c6 100644 (file)
@@ -8,7 +8,7 @@
                 { field=>'by_default',  type=>'checkbox', value=>'Y' },
                 $tagcolor,
               ],
-              'labels'        => { 'tagnum'   => 'Tag #',
+              'labels'        => { 'tagnum'   => 'Tag',
                                    'tagname'  => 'Tag',
                                    'tagdesc'  => 'Message',
                                    'tagcolor' => 'Highlight Color',
index dfe52f1..a469beb 100644 (file)
@@ -19,7 +19,7 @@
 
 
 <SCRIPT TYPE="text/javascript">
-  var modulesForNamespace = <% to_json(\%modules_for_namespace, {canonical=>1}) %>;
+  var modulesForNamespace = <% encode_json(\%modules_for_namespace, {canonical=>1}) %>;
   function changeNamespace(what) {
     var ns = what.value;
     var select_module = document.getElementById('gateway_module');
index 4aec63e..7bc88a8 100644 (file)
 %>
 <%init>
 
-my @deviceparts_with_inventory;
-my @part_device = qsearch('part_device', {} );
-foreach my $part_device ( @part_device ) {
-    push @deviceparts_with_inventory, $part_device->devicepart
-       if $part_device->inventory_classnum;
-}
+my @deviceparts_with_inventory =
+  map $_->devicepart,
+    qsearch({ 'table'     => 'part_device',
+              'extra_sql' => 'WHERE inventory_classnum IS NOT NULL',
+           });
 
 my $html_foot = sub {
     my $js = "
@@ -72,9 +71,9 @@ my $html_foot = sub {
 
        var devicepart = what.options[what.selectedIndex].value;
 
-       var deviceparts_with_inventory = new Array(\"";
-$js .= join("\",\"",@deviceparts_with_inventory);
-$js .= "\");
+       var deviceparts_with_inventory = new Array(";
+$js .= join(',', map qq("$_"), @deviceparts_with_inventory);
+$js .= ");
 
        var hasInventory = false;
        for ( i = 0; i < deviceparts_with_inventory.length; i++ ) {
index 3e0ef59..fd28934 100755 (executable)
@@ -19,36 +19,41 @@ die "access denied"
 my $pkgnum = $cgi->param('pkgnum') or die;
 my $old = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
 my %hash = $old->hash;
-$hash{$_}= $cgi->param($_) ? parse_datetime($cgi->param($_)) : ''
-  foreach qw( start_date setup bill last_bill contract_end );
+foreach ( qw( start_date setup bill last_bill contract_end ) ) {
+  if ( $cgi->param($_) =~ /^(\d+)$/ ) {
+    $hash{$_} = $1;
+  } else {
+    $hash{$_} = '';
+  }
   # adjourn, expire, resume not editable this way
-
-my @errors = ();
-
-push @errors, '_bill_areyousure'
-  if $hash{'bill'} != $old->bill             # if the next bill date was changed
-  && $hash{'bill'} < time                    # to a date in the past
-  && ! $cgi->param('bill_areyousure');       # and it wasn't confirmed
-
-push @errors, '_setup_areyousure'
-  if ! $hash{'setup'} && $old->setup         # if the setup date was removed
-  && ! $cgi->param('setup_areyousure');      # and it wasn't confirmed 
-
-push @errors, '_setupadd_areyousure'
-  if $hash{'setup'} && ! $old->setup         # if the setup date was added
-  && ! $cgi->param('setupadd_areyousure');   # and it wasn't confirmed 
-
-push @errors, '_start'
-  if $hash{'start_date'} && !$old->start_date # if a start date was added
-  && $hash{'setup'};                          # but there's a setup date
+}
 
 my $new;
 my $error;
-if ( @errors ) {
-  $error = join(',', @errors);
-} else {
-  $new = new FS::cust_pkg \%hash;
-  $error = $new->replace($old);
+$new = new FS::cust_pkg \%hash;
+$error = $new->replace($old);
+
+if (!$error) {
+  my @supp_pkgs = $old->supplemental_pkgs;
+  foreach $new (@supp_pkgs) {
+    foreach ( qw( start_date setup contract_end ) ) {
+      # propagate these to supplementals
+      $new->set($_, $hash{$_});
+    }
+    if ( $hash{'bill'} ne $old->get('bill') ) {
+      if ( $hash{'bill'} and $old->get('bill') ) {
+        # adjust by the same interval
+        my $diff = $hash{'bill'} - $old->get('bill');
+        $new->set('bill', $new->get('bill') + $diff);
+      } else {
+        # absolute date
+        $new->set('bill', $hash{'bill'});
+      }
+    }
+    $error = $new->replace;
+    $error .= ' (supplemental package '.$new->pkgnum.')' if $error; 
+    last if $error;
+  }
 }
 
 </%init>
diff --git a/httemplate/edit/process/bulk-part_pkg.html b/httemplate/edit/process/bulk-part_pkg.html
new file mode 100644 (file)
index 0000000..4775a93
--- /dev/null
@@ -0,0 +1,30 @@
+% if ( $error ) {
+%  $cgi->param('error', $error);
+<% $cgi->redirect(popurl(3).'/edit/bulk-part_pkg.cgi?', $cgi->query_string) %>
+% } else {
+<% $cgi->redirect(popurl(3).'/browse/part_pkg.cgi') %>
+% }
+<%init>
+die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Bulk edit package definitions');
+
+my @pkgparts = $cgi->param('pkgpart')
+  or die "no package definitions selected";
+
+my %changes;
+foreach my $param (grep { /^report_option_\d+$/ } $cgi->param) {
+  if ( length($cgi->param($param)) ) {
+    if ( $cgi->param($param) == 1 ) {
+      $changes{$param} = 1;
+    } else {
+      $changes{$param} = '';
+    }
+  }
+}
+
+my $error;
+foreach my $pkgpart (@pkgparts) {
+  my $part_pkg = FS::part_pkg->by_key($pkgpart);
+  my %options = ( $part_pkg->options, %changes );
+  $error ||= $part_pkg->replace( options => \%options );
+}
+</%init>
index 2770f32..77f261d 100644 (file)
@@ -32,11 +32,11 @@ my %change = map { $_ => scalar($cgi->param($_)) }
 $change{'keep_dates'} = 1;
 
 if ( $cgi->param('locationnum') == -1 ) {
-  my $cust_location = new FS::cust_location {
+  my $cust_location = FS::cust_location->new_or_existing({
     'custnum' => $cust_pkg->custnum,
     map { $_ => scalar($cgi->param($_)) }
         qw( address1 address2 city county state zip country )
-  };
+  });
   $change{'cust_location'} = $cust_location;
 }
 
index cbcf619..8e66368 100644 (file)
@@ -10,7 +10,7 @@
 <%init>
 
 die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('Post credit');
+  unless $FS::CurrentUser::CurrentUser->access_right('Credit line items');
 
 my @billpkgnum_setuprecurs =
   map { $_ =~ /^billpkgnum(\d+\-\w*)$/ or die 'gm#23'; $1; } 
index b9f93db..56c3968 100644 (file)
@@ -28,11 +28,10 @@ my $cust_location = qsearchs({
 });
 die "unknown locationnum $locationnum" unless $cust_location;
 
-my $new = FS::cust_location->new({
+my $new = FS::cust_location->new_or_existing({
   custnum     => $cust_location->custnum,
   prospectnum => $cust_location->prospectnum,
-  map { $_ => scalar($cgi->param($_)) }
-    qw( address1 address2 city county state zip country )
+  map { $_ => scalar($cgi->param($_)) } FS::cust_main->location_fields
 });
 
 my $error = $cust_location->move_to($new);
index 31ec4ab..c1f8155 100755 (executable)
@@ -16,8 +16,8 @@ my $DEBUG = 0;
 </%once>
 <%init>
 
-die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('Edit customer');
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied" unless $curuser->access_right('Edit customer');
 
 my $conf = new FS::Conf;
 
@@ -62,6 +62,18 @@ $cgi->param('invoicing_list', join(',', @invoicing_list) );
 $cgi->param('duplicate_of_custnum') =~ /^(\d+)$/;
 my $duplicate_of = $1;
 
+# if this is enabled, enforce it
+if ( $conf->exists('agent-ship_address', $cgi->param('agentnum')) ) {
+  my $agent = FS::agent->by_key($cgi->param('agentnum'));
+  my $agent_cust_main = $agent->agent_cust_main;
+  if ( $agent_cust_main ) {
+    my $agent_location = $agent_cust_main->ship_location;
+    foreach (qw(address1 city state zip country latitude longitude district)) {
+      $cgi->param("ship_$_", $agent_location->get($_));
+    }
+  }
+}
+
 my %locations;
 for my $pre (qw(bill ship)) {
 
@@ -71,10 +83,7 @@ for my $pre (qw(bill ship)) {
   }
   $hash{'custnum'} = $cgi->param('custnum');
   warn Dumper \%hash if $DEBUG;
-  # if we can qsearchs it, then it's unchanged, so use that
-  $locations{$pre} = qsearchs('cust_location', \%hash)
-                     || FS::cust_location->new( \%hash );
-
+  $locations{$pre} = FS::cust_location->new_or_existing(\%hash);
 }
 
 if ( ($cgi->param('same') || '') eq 'Y' ) {
@@ -156,9 +165,14 @@ foreach my $dfield (qw(
 $new->setfield('paid', $cgi->param('paid') )
   if $cgi->param('paid');
 
-my @exempt_groups = grep /\S/, $conf->config('tax-cust_exempt-groups');
-my @tax_exempt = grep { $cgi->param("tax_$_") eq 'Y' } @exempt_groups;
-my %tax_exempt = map { $_ => scalar($cgi->param("tax_$_".'_num')) } @tax_exempt;
+my %options = ();
+if ( $curuser->access_right('Edit customer tax exemptions') ) { 
+  my @exempt_groups = grep /\S/, $conf->config('tax-cust_exempt-groups');
+  my @tax_exempt = grep { $cgi->param("tax_$_") eq 'Y' } @exempt_groups;
+  $options{'tax_exemption'} = {
+    map { $_ => scalar($cgi->param("tax_$_".'_num')) } @tax_exempt
+  };
+}
 
 #perhaps this stuff should go to cust_main.pm
 if ( $new->custnum eq '' or $duplicate_of ) {
@@ -266,8 +280,8 @@ if ( $new->custnum eq '' or $duplicate_of ) {
   else {
     # create the customer
     $error ||= $new->insert( \%hash, \@invoicing_list,
-                           'tax_exemption'=> \%tax_exempt,
-                           'prospectnum'  => scalar($cgi->param('prospectnum')),
+                             %options,
+                             prospectnum => scalar($cgi->param('prospectnum')),
                            );
 
     my $conf = new FS::Conf;
@@ -328,7 +342,7 @@ if ( $new->custnum eq '' or $duplicate_of ) {
   warn Dumper({ new => $new, old => $old }) if $DEBUG;
 
   $error ||= $new->replace( $old, \@invoicing_list,
-                            'tax_exemption' => \%tax_exempt,
+                            %options,
                           );
 
   warn "$me returned from replace" if $DEBUG;
diff --git a/httemplate/edit/process/cust_pkg_quantity.html b/httemplate/edit/process/cust_pkg_quantity.html
new file mode 100644 (file)
index 0000000..fb26572
--- /dev/null
@@ -0,0 +1,33 @@
+% if ($error) {
+%   $cgi->param('error', $error);
+%   $cgi->redirect(popurl(3). 'edit/cust_pkg_quantity.html?'. $cgi->query_string );
+% } else {
+
+    <& /elements/header-popup.html, "Quantity changed" &>
+      <SCRIPT TYPE="text/javascript">
+        window.top.location.reload();
+      </SCRIPT>
+    </BODY>
+    </HTML>
+
+% }
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('Change customer package');
+
+my $cust_pkg = qsearchs({
+  'table'     => 'cust_pkg',
+  'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+  'hashref'   => { 'pkgnum' => scalar($cgi->param('pkgnum')), },
+  'extra_sql' => ' AND '. $curuser->agentnums_sql,
+});
+die 'unknown pkgnum' unless $cust_pkg;
+
+$cgi->param('quantity') =~ /^(\d+)$/;
+my $quantity = $1;
+my $error = $cust_pkg->set_quantity($1);
+
+</%init>
index e22cbb2..7cb1d6d 100644 (file)
@@ -6,7 +6,7 @@
 %}
 <%init>
 
-die 'access deined'
+die 'access denied'
  unless $FS::CurrentUser::CurrentUser->access_right('Change customer service');
 
 my $svcnum = $cgi->param('svcnum');
index 2d39e9d..fb1ee7a 100644 (file)
@@ -263,6 +263,9 @@ foreach my $value ( @values ) {
 
     if ( !$error ) {
       if ( $old_pkey ) {
+
+        &{ $opt{'edit_callback'} }( $new, $old ) if $opt{'edit_callback'};
+
         $error = $new->replace($old, @args);
       } else {
         $error = $new->insert(@args);
index 5a8afbd..06f4c00 100644 (file)
@@ -10,5 +10,10 @@ my %opt = @_;
 my $table = $opt{'table'};
 $opt{'fields'} ||= [ fields($table) ];
 push @{ $opt{'fields'} }, qw( pkgnum svcpart );
+foreach (fields($table)) {
+  if ( $cgi->param($_.'_classnum') ) {
+    push @{ $opt{'fields'} }, $_.'_classnum';
+  }
+}
 
 </%init>
index bcb9c0d..e0c4706 100644 (file)
@@ -56,6 +56,7 @@ my $new = new FS::part_export ( {
 if ( $cgi->param('svc_machine') eq 'Y' ) {
   $new->machine('_SVC_MACHINE');
   $new->part_export_machine_textarea( $cgi->param('part_export_machine') );
+  $new->default_machine_name( $cgi->param('default_machine_name') );
 }
 
 my $error;
index c388676..932e33b 100755 (executable)
@@ -10,6 +10,7 @@
               'precheck_callback' => $precheck_callback,
               'args_callback'     => $args_callback,
               'process_m2m'       => \@process_m2m,
+              'process_o2m'       => \@process_o2m,
           )
 %>
 <%init>
@@ -185,6 +186,15 @@ my @process_m2m = (
                         grep /^svc_dst_pkgpart/, $cgi->param
                       ],
   },
+  { 'link_table'   => 'part_pkg_link',
+    'target_table' => 'part_pkg',
+    'base_field'   => 'src_pkgpart',
+    'target_field' => 'dst_pkgpart',
+    'hashref'      => { 'link_type' => 'supp', 'hidden' => '' },
+    'params'       => [ map $cgi->param($_),
+                        grep /^supp_dst_pkgpart/, $cgi->param
+                      ],
+  },
   map { 
     my $hidden = $_;
     { 'link_table'   => 'part_pkg_link',
@@ -235,4 +245,11 @@ if ( $cgi->param('pkgpart') || ! $conf->exists('agent_defaultpkg') ) {
   };
 }
 
+my @process_o2m = (
+  {
+    'table'  => 'part_pkg_msgcat',
+    'fields' => [qw( locale pkg )],
+  },
+);
+
 </%init>
diff --git a/httemplate/edit/process/part_pkg_usage.html b/httemplate/edit/process/part_pkg_usage.html
new file mode 100644 (file)
index 0000000..eb6c37b
--- /dev/null
@@ -0,0 +1,67 @@
+% if ( $is_error ) {
+%   $cgi->param('error' => \%part_pkg_usage);
+% # internal redirect, because it's a lot of state to pass through
+<& /browse/part_pkg_usage.html &>
+% } else {
+% # uh, not quite sure...
+<%  $cgi->redirect($fsurl.'browse/part_pkg.cgi') %>
+% }
+<%init>
+my %vars = $cgi->Vars;
+my %part_pkg_usage;
+my $is_error;
+foreach my $pkgpart ($cgi->param('pkgpart')) {
+  next unless $pkgpart =~ /^\d+$/;
+  my $part_pkg = FS::part_pkg->by_key($pkgpart)
+    or die "unknown pkgpart $pkgpart";
+  my %old = map { $_->pkgusagepart => $_ } $part_pkg->part_pkg_usage;
+  $part_pkg_usage{$pkgpart} ||= [];
+  my @rows;
+  foreach (grep /^pkgpart$pkgpart/, keys %vars) {
+    /^pkgpart\d+_(\w+\D)(\d+)$/ or die "misspelled field name '$_'";
+    my $value = delete $vars{$_};
+    my $field = $1;
+    my $row = $2;
+    $rows[$row] ||= {};
+    $rows[$row]->{$field} = $value;
+  }
+
+  foreach my $row (@rows) {
+    next if !defined($row);
+    my $error;
+    my %classes;
+    foreach my $class (grep /^class/, keys %$row) {
+      $class =~ /^class(\d+)_$/;
+      my $classnum = $1;
+      $classes{$classnum} = delete $row->{$class};
+    }
+    my $usage = FS::part_pkg_usage->new($row);
+    $usage->set('pkgpart', $pkgpart);
+    if ( $usage->pkgusagepart and $row->{minutes} > 0 ) {
+      $error = $usage->replace(\%classes);
+      # and don't delete the existing one
+      delete($old{$usage->pkgusagepart});
+    } elsif ( $row->{minutes} > 0 ) {
+      $error = $usage->insert(\%classes);
+    } else {
+      next;
+    }
+    if ( $error ) {
+      $usage->set('error', $error);
+      $is_error = 1;
+    }
+    push @{ $part_pkg_usage{$pkgpart} }, $usage;
+  }
+
+  foreach my $usage (values %old) {
+    # all of these were not sent back by the client, so delete them
+    my $error = $usage->delete;
+    if ( $error ) {
+      $usage->set('error', $error);
+      $is_error = 1;
+      unshift @{ $part_pkg_usage{$pkgpart} }, $usage;
+    }
+  }
+
+}
+</%init>
index 2dadbcc..0cc17d3 100644 (file)
@@ -70,6 +70,9 @@ my $quantity = $1 || 1;
 $cgi->param('refnum') =~ /^(\d*)$/
   or die 'illegal refnum '. $cgi->param('refnum');
 my $refnum = $1;
+$cgi->param('contactnum') =~ /^(\-?\d*)$/
+  or die 'illegal contactnum '. $cgi->param('contactnum');
+my $contactnum = $1;
 $cgi->param('locationnum') =~ /^(\-?\d*)$/
   or die 'illegal locationnum '. $cgi->param('locationnum');
 my $locationnum = $1;
@@ -109,6 +112,7 @@ my %hash = (
                                   : ''
                               ),
     'refnum'               => $refnum,
+    'contactnum'           => $contactnum,
     'locationnum'          => $locationnum,
     'discountnum'          => $discountnum,
     #for the create a new discount case
@@ -142,11 +146,19 @@ if ( $quotationnum ) {
 
   my %opt = ( 'cust_pkg' => $cust_pkg );
 
+  if ( $contactnum == -1 ) {
+    my $contact = FS::contact->new({
+      'custnum' => scalar($cgi->param('custnum')),
+      map { $_ => scalar($cgi->param("contactnum_$_")) } qw( first last )
+    });
+    $opt{'contact'} = $contact;
+  }
+
   if ( $locationnum == -1 ) {
-    my $cust_location = new FS::cust_location {
+    my $cust_location = FS::cust_location->new_or_existing({
       map { $_ => scalar($cgi->param($_)) }
-          qw( custnum address1 address2 city county state zip country geocode )
-    };
+          ('custnum', FS::cust_main->location_fields)
+    });
     $opt{'cust_location'} = $cust_location;
   }
 
index 7a3b43d..9983ea2 100644 (file)
@@ -2,6 +2,7 @@
                'table'          => 'svc_phone',
                'args_callback'  => $args_callback,
               'value_callback' => $value_callback,
+               'edit_callback'  => $edit_callback,
                %opt,
 &>
 <%init>
@@ -28,6 +29,9 @@ my $right = $opt{'bulk'} ? 'Bulk provision customer service'
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right($right);
 
+$cgi->param('phonenum', $cgi->param('phonenum_manual') )
+  if $cgi->param('phonenum_which') eq 'phonenum_manual';
+
 my $tollfreephonenum = $cgi->param('tollfreephonenum');
 $cgi->param('phonenum',$tollfreephonenum) if $tollfreephonenum =~ /^\d+$/;
 
@@ -36,10 +40,10 @@ my $args_callback = sub {
 
   my %opt = ();
   if ( $cgi->param('locationnum') == -1 ) {
-    my $cust_location = new FS::cust_location {
+    my $cust_location = FS::cust_location->new_or_existing({
       map { $_ => scalar($cgi->param($_)) }
           qw( custnum address1 address2 city county state zip country )
-    };
+    });
     $opt{'cust_location'} = $cust_location;
   }
 
@@ -48,8 +52,13 @@ my $args_callback = sub {
 };
 
 my $value_callback = sub {
-     my ($field, $value) = @_;
-     ($field =~ /_date$/) ? parse_datetime($value) : $value;
+  my ($field, $value) = @_;
+  ($field =~ /_date$/) ? parse_datetime($value) : $value;
+};
+
+my $edit_callback = sub {
+  my( $new, $old ) = @_;
+  $new->sip_password( $old->sip_password ) if $new->sip_password eq '*HIDDEN*';
 };
 
 </%init>
index 1d9647f..466091d 100644 (file)
@@ -145,7 +145,6 @@ function bill_now_changed (what) {
     <% mt('with terms') |h %> 
     <& /elements/select-terms.html,
                  'curr_value'  => scalar($cgi->param('invoice_terms')),
-                 'empty_value' => $default_terms,
                  'disabled'    => ( $cgi->param('bill_now') ? 0 : 1 ),
     &>
   </TD>
index 367bbaf..a1c1bcb 100644 (file)
     </TD>
   </TR>
 
+  <& /elements/tr-checkbox.html,
+    label       => 'Exact match',
+    field       => 'exact_match',
+    cell_style  => 'font-weight: bold',
+    value       => 'Y',
+    curr_value  => $rate_region->exact_match
+  &>
+
 </TABLE>
 
 <BR>
index c1f7455..627791b 100755 (executable)
@@ -9,19 +9,6 @@
   <BR>
 % } 
 
-<SCRIPT TYPE="text/javascript">
-function randomPass() {
-  var i=0;
-  var pw_set='<% join('', 'a'..'z', 'A'..'Z', '0'..'9' ) %>';
-  var pass='';
-  while(i < 8) {
-    i++;
-    pass += pw_set.charAt(Math.floor(Math.random() * pw_set.length));
-  }
-  document.OneTrueForm.clear_password.value = pass;
-}
-</SCRIPT>
-
 <FORM NAME="OneTrueForm" ACTION="<% $p1 %>process/svc_acct.cgi" METHOD=POST>
 <INPUT TYPE="hidden" NAME="svcnum" VALUE="<% $svcnum %>">
 <INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
@@ -57,10 +44,11 @@ function randomPass() {
 
 %if ( $part_svc->part_svc_column('_password')->columnflag ne 'F' ) {
 <TR>
+% #XXX eventually should require "Edit Password" ACL
   <TD ALIGN="right"><% mt('Password') |h %></TD>
   <TD>
-    <INPUT TYPE="text" NAME="clear_password" VALUE="<% $password %>" SIZE=<% $pmax2 %> MAXLENGTH=<% $pmax %>>
-    <INPUT TYPE="button" VALUE="<% mt('Generate') |h %>" onclick="randomPass();">
+    <INPUT TYPE="text" ID="clear_password" NAME="clear_password" VALUE="<% $password %>" SIZE=<% $pmax2 %> MAXLENGTH=<% $pmax %>>
+    <& /elements/random_pass.html, 'clear_password' &>
   </TD>
 </TR>
 %}else{
index 0d4b989..1b85460 100644 (file)
@@ -104,8 +104,12 @@ my @fields = (
   { field=>'sectornum', type=>'select-tower_sector', },
   { field=>'routernum', type=>'select-router_block_ip' },
   { field=>'mac_addr' , type=>'input-mac_addr' },
-    qw( latitude longitude altitude vlan_profile 
-    performance_profile authkey plan_id )
+  qw(
+      latitude longitude altitude
+      radio_serialnum radio_location poe_location rssi suid
+    ),
+  { field=>'shared_svcnum', type=>'search-svc_broadband', },
+  qw( vlan_profile performance_profile authkey plan_id ),
 );
 
 if ( $conf->exists('svc_broadband-radius') ) {
index 9647b68..13bbe82 100644 (file)
@@ -6,6 +6,11 @@
        my( $cgi, $svc_x, $part_svc, $cust_pkg, $fields, $opt ) = @_;
        $svc_x->locationnum($cust_pkg->locationnum) if $cust_pkg;
      },
+     'svc_edit_callback' => sub {
+       my( $cgi, $svc_x, $part_svc, $cust_pkg, $fields, $opt) = @_;
+       my $conf = new FS::Conf;
+       $svc_x->sip_password('*HIDDEN*') unless $conf->exists('showpasswords');
+     },
 &>
 <%init>
 
@@ -28,6 +33,11 @@ my $begin_callback = sub {
                 type     => 'select-did',
                 label    => 'Phone number',
                 multiple => $bulk,
+              },
+              { field     => 'sim_imsi',
+                type      => 'text',
+                size      => 15,
+                maxlength => 15,
               };
 
   push @$fields, { field => 'domsvc',
index 9aff94e..5118b91 100644 (file)
@@ -50,7 +50,7 @@ var <%$pre%>next_rownum;
 var <%$pre%>set_rownum;
 var <%$pre%>addRow;
 var <%$pre%>deleteRow;
-var <%$pre%>fieldorder = <% to_json($fieldorder) %>;
+var <%$pre%>fieldorder = <% encode_json($fieldorder) %>;
 
 function <%$pre%>possiblyAddRow_factory(obj) {
   var callback = obj.onchange;
@@ -70,8 +70,8 @@ function <%$pre%>set_rownum(obj, rownum) {
   if ( obj.id ) {
     obj.id = obj.id + rownum;
   }
-  if ( obj.name ) {
-    obj.name = obj.name + rownum;
+  if ( obj.getAttribute('name') ) {
+    obj.setAttribute('name', obj.getAttribute('name') + rownum);
     // also, in this case it's a form field that will be part of the record
     // so set up an onchange handler
     obj.onchange = <%$pre%>possiblyAddRow_factory(obj);
@@ -96,17 +96,32 @@ function <%$pre%>addRow(data) {
   <%$pre%>set_rownum(row, this_rownum);
   if(data instanceof Array) {
     for (i = 0; i < data.length && i < <%$pre%>fieldorder.length; i++) {
-      var el = document.getElementsByName(<%$pre%>fieldorder[i] + this_rownum)[0];
+      var el = document.getElementsByName(<%$pre |js_string%> +
+                                          <%$pre%>fieldorder[i] +
+                                          this_rownum)[0];
       if (el) {
-        el.value = data[i];
+        if ( el.tagName.toLowerCase() == 'span' ) {
+          el.innerHTML = data[i];
+        } else if ( el.type == 'checkbox' ) {
+          el.checked = (el.value == data[i]);
+        } else {
+          el.value = data[i];
+        }
       }
     }
   } else if (data instanceof Object) {
     for (var field in data) {
-      var el = document.getElementsByName(field + this_rownum)[0];
+      var el = document.getElementsByName(<%$pre |js_string%> +
+                                          field +
+                                          this_rownum)[0];
       if (el) {
-        el.value = data[field];
-%       # doesn't work for checkbox
+        if ( el.tagName.toLowerCase() == 'span' ) {
+          el.innerHTML = data[field];
+        } else if ( el.type == 'checkbox' ) {
+          el.checked = (el.value == data[field]);
+        } else {
+          el.value = data[field];
+        }
       }
     }
   } // else nothing
@@ -123,6 +138,20 @@ function <%$pre%>deleteRow(rownum) {
   <%$pre%>tbody.removeChild(r);
 }
 
+function <%$pre%>set_prefix(obj) {
+  if ( obj.id ) {
+    obj.id = <%$pre |js_string%> + obj.id;
+  }
+  if ( obj.getAttribute('name') ) {
+    obj.setAttribute('name', <%$pre |js_string%> + obj.getAttribute('name'));
+  }
+  for (var i = 0; i < obj.children.length; i++) {
+    if ( obj.children[i] instanceof Node ) {
+      <%$pre%>set_prefix(obj.children[i]);
+    }
+  }
+}
+
 function <%$pre%>init() {
   <%$pre%>template = document.getElementById(<% $template_row |js_string%>);
   <%$pre%>tbody = document.getElementById('<%$pre%>autotable');
@@ -131,8 +160,10 @@ function <%$pre%>init() {
   var table = <%$pre%>template.parentNode;
   table.removeChild(<%$pre%>template);
   // give it an id
-  <%$pre%>template.id = <%$pre |js_string%> + 'row';
-  // and a magic identifier so we know it's been submitted
+  <%$pre%>template.id = 'row';
+  // prefix the ids and names of the TR object and all its descendants
+  <%$pre%>set_prefix(<%$pre%>template);
+  // add a magic identifier so we know it's been submitted
   var magic = document.createElement('INPUT');
   magic.setAttribute('type', 'hidden');
   magic.setAttribute('name', '<%$pre%>magic');
@@ -140,18 +171,26 @@ function <%$pre%>init() {
   // and a delete button
 %# should this be enclosed in an actual <button> for aesthetics?
   var delete_button = document.createElement('IMG');
-  delete_button.id = 'delete_button';
+  delete_button.id = '<%$pre%>delete_button';
   delete_button.src = '<%$fsurl%>images/cross.png';
   delete_button.alt = 'X';
   // use an inline string for this so that it will be cloned properly
   delete_button.setAttribute('onclick', "<%$pre%>deleteRow(this.rownum);");
+  // and an error display
+  var error_span = document.createElement('SPAN');
+  error_span.className = 'error';
+  error_span.style.color = '#FF0000';
+  error_span.setAttribute('name', '<%$pre%>error');
+  error_span.style.padding = '5px';
   var delete_cell = document.createElement('TD');
+  delete_cell.style.textAlign = 'left';
   delete_cell.appendChild(delete_button);
   delete_cell.appendChild(magic); // it has to go somewhere
+  delete_cell.appendChild(error_span);
   <%$pre%>template.appendChild(delete_cell);
 
   // preload rows
-  var rows = <% to_json(\@rows) %>;
+  var rows = <% encode_json(\@rows) %>;
   for (var i = 0; i < rows.length; i++) {
     <%$pre%>addRow(rows[i]);
   }
index 232664e..34ce70b 100644 (file)
   <TH CLASS="grid" BGCOLOR="#cccccc">Description</TH>
 </TR>
 
-% foreach my $item ( sort { $a->history_date <=> $b->history_date
-%                           #|| table order
-%                           || $a->historynum <=> $b->historynum
-%                         }
-%                         @history
-%                  )
-% {
+% foreach my $item ( @history ) {
 %   my $history_other = '';
 %   my $act  = $item->history_action;
 %   if ( $act =~ /^replace/ ) {
@@ -196,4 +190,11 @@ $cust_pkg_date_format .= ' %l:%M:%S%P'
   if $conf->exists('cust_pkg-display_times')
   || $curuser->option('cust_pkg-display_times');
 
+@history = sort { $a->history_date <=> $b->history_date
+                  || $a->historynum <=> $b->historynum } @history;
+
+if ( $curuser->option('history_order') eq 'newest' ) {
+  @history = reverse @history;
+}
+
 </%init>
diff --git a/httemplate/elements/change_password.html b/httemplate/elements/change_password.html
new file mode 100644 (file)
index 0000000..625ba1f
--- /dev/null
@@ -0,0 +1,41 @@
+<STYLE>
+.passwordbox {
+  border: 1px solid #7e0079;
+  padding: 2px;
+  position: absolute;
+  font-size: 80%;
+  background-color: #ffffff;
+  display: none;
+}
+</STYLE>
+<A ID="<%$pre%>link" HREF="#" onclick="<%$pre%>toggle(true)">(<% mt('change') %>)</A>
+<DIV ID="<%$pre%>form" CLASS="passwordbox">
+  <FORM METHOD="POST" ACTION="<%$fsurl%>misc/process/change-password.html">
+    <INPUT TYPE="hidden" NAME="svcnum" VALUE="<% $svc_acct->svcnum |h%>">
+    <INPUT TYPE="text" ID="<%$pre%>password" NAME="password" VALUE="<% $curr_value |h%>">
+    <& /elements/random_pass.html, $pre.'password', 'randomize' &>
+    <INPUT TYPE="submit" VALUE="change">
+    <INPUT TYPE="button" VALUE="cancel" onclick="<%$pre%>toggle(false)">
+% if ( $error ) {
+    <BR><SPAN STYLE="color: #ff0000"><% $error |h %></SPAN>
+% }
+  </FORM>
+</DIV>
+<SCRIPT TYPE="text/javascript">
+function <%$pre%>toggle(val) {
+  document.getElementById('<%$pre%>form').style.display =
+    val ? 'inline-block' : 'none';
+  document.getElementById('<%$pre%>link').style.display =
+    val ? 'none' : 'inline';
+}
+% if ( $error ) {
+<%$pre%>toggle(true);
+% }
+</SCRIPT>
+<%init>
+my %opt = @_;
+my $svc_acct = $opt{'svc_acct'};
+my $curr_value = $opt{'curr_value'} || '';
+my $pre = 'changepw'.$svc_acct->svcnum.'_';
+my $error = $cgi->param($pre.'error');
+</%init>
diff --git a/httemplate/elements/checkbox-tristate.html b/httemplate/elements/checkbox-tristate.html
new file mode 100644 (file)
index 0000000..4c26ed7
--- /dev/null
@@ -0,0 +1,78 @@
+<%doc>
+A tristate checkbox (with three values: true, false, and null).
+Internally, this creates a checkbox, coupled via javascript to a hidden
+field that actually contains the value.  For now, the only values these
+can have are 1, 0, and empty.  Clicking the checkbox cycles between them.
+</%doc>
+<%shared>
+my $init = 0;
+</%shared>
+% if ( !$init ) {
+%   $init = 1;
+<SCRIPT TYPE="text/javascript">
+function tristate_onclick() {
+  var checkbox = this;
+  var input = checkbox.input;
+  if ( input.value == "" ) {
+    input.value = "0";
+    checkbox.checked = false;
+    checkbox.indeterminate = false;
+  } else if ( input.value == "0" ) {
+    input.value = "1";
+    checkbox.checked = true;
+    checkbox.indeterminate = false;
+  } else if ( input.value == "1" ) {
+    input.value = "";
+    checkbox.checked = true;
+    checkbox.indeterminate = true
+  }
+}
+
+var tristates = [];
+var tristate_boxes = [];
+window.onload = function() { // don't do this until all of the checkboxes exist
+%#  tristates = document.getElementsByClassName('tristate'); # curse you, IE8
+  var all_inputs = document.getElementsByTagName('input');
+  for (var i=0; i < all_inputs.length; i++) {
+    if ( all_inputs[i].className == 'tristate' ) {
+      tristates.push(all_inputs[i]);
+    }
+  }
+  for (var i=0; i < tristates.length; i++) {
+    tristate_boxes[i] =
+      document.getElementById('checkbox_' + tristates[i].name);
+    // make sure they can find each other
+    tristate_boxes[i].input = tristates[i];
+    tristates[i].checkbox = tristate_boxes[i];
+    // set event handler
+    tristate_boxes[i].onclick = tristate_onclick;
+    // set initial value
+    if ( tristates[i].value == "" ) {
+      tristate_boxes[i].indeterminate = true
+    }
+    if ( tristates[i].value != "0" ) {
+      tristate_boxes[i].checked = true;
+    }
+  }
+};
+</SCRIPT>
+% } # end of $init
+<INPUT TYPE="hidden" NAME="<% $opt{field} %>"
+                     ID="<% $opt{id} %>"
+                     VALUE="<% $curr_value %>"
+                     CLASS="tristate">
+<INPUT TYPE="checkbox" ID="checkbox_<%$opt{field}%>" CLASS="partial">
+<%init>
+
+my %opt = @_;
+
+# might be useful but I'm not implementing it yet
+#my $onchange = $opt{'onchange'}
+#                 ? 'onChange="'. $opt{'onchange'}. '(this)"'
+#                 : '';
+
+$opt{'id'} ||= 'hidden_'.$opt{'field'};
+my $curr_value = $opt{curr_value};
+$curr_value = undef
+  unless $curr_value eq '0' or $curr_value eq '1';
+</%init>
index 490ba23..3d51776 100644 (file)
@@ -2,9 +2,9 @@
 
   <INPUT TYPE="hidden" NAME="<%$name%>" ID="<%$id%>" VALUE="<% $curr_value %>">
 
-  <TABLE>
+  <TABLE STYLE="display:inline">
     <TR>
-%     if ( @contact_class ) {
+%     if ( @contact_class && ! $opt{name_only} ) {
         <TD>
           <SELECT NAME="<%$name%>_classnum" <% $onchange %>>
             <OPTION VALUE="">
@@ -106,6 +106,6 @@ foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) {
 
 $label{'comment'} = 'Comment';
 
-my @fields = keys %label;
+my @fields = $opt{'name_only'} ? qw( first last ) : keys %label;
 
 </%init>
index f4a3725..b80af78 100644 (file)
@@ -169,7 +169,6 @@ if ( $FS::TicketSystem::system eq 'RT_Internal'
           ObjectCustomFieldValues.ObjectId = cust_tickets.Id
         )
       GROUP BY cust_tickets.custnum, ObjectCustomFieldValues.Content";
-      #warn $sql."\n";
   } else { # no custom_priority_field
     $sql =
     "SELECT cust_tickets.custnum,
@@ -181,10 +180,8 @@ if ( $FS::TicketSystem::system eq 'RT_Internal'
   my $sth = dbh->prepare($sql) or die dbh->errstr;
   $sth->execute or die $sth->errstr;
   while ( my $row = $sth->fetchrow_hashref ) {
-  #warn to_json($row)."\n";
     $num_tickets_by_priority{ $row->{priority} }->{ $row->{custnum} } =
       $row->{num_tickets};
   }
 }
-#warn Dumper \%num_tickets_by_priority;
 </%init>
index 8e0126b..eb7d339 100644 (file)
@@ -304,7 +304,7 @@ function FCKeditor_IsCompatibleBrowser()
        // Internet Explorer 5.5+\r
        if ( /*@cc_on!@*/false && sAgent.indexOf("mac") == -1 )\r
        {\r
-               var sBrowserVersion = navigator.appVersion.match(/MSIE (.\..)/)[1] ;\r
+               var sBrowserVersion = navigator.appVersion.match(/MSIE ([\d.]+)/)[1] ;\r
                return ( sBrowserVersion >= 5.5 ) ;\r
        }\r
 \r
index 873fe16..6855233 100644 (file)
@@ -200,7 +200,7 @@ Example:
 </TR>
 % } else {
 %   foreach (qw(latitude longitude)) {
-<INPUT TYPE="hidden" NAME="<% $_ %>" VALUE="<% $object->get($_) |h%>">
+<INPUT TYPE="hidden" NAME="<% $_ %>" ID="<% $_ %>" VALUE="<% $object->get($_) |h%>">
 %   }
 % }
 <INPUT TYPE="hidden" NAME="<%$pre%>coord_auto" VALUE="<% $object->coord_auto %>">
@@ -226,12 +226,13 @@ Example:
     <TD COLSPAN=8>
       <INPUT TYPE="text" SIZE=15
              NAME="<%$pre%>district" 
+             ID="<%$pre%>district"
              VALUE="<% $object->district |h %>">
     <% '(automatic)' %>
     </TD>
   </TR>
 %   } else {
-    <INPUT TYPE="hidden" NAME="<%$pre%>district" VALUE="<% $object->district %>">
+    <INPUT TYPE="hidden" ID="<%$pre%>" NAME="<%$pre%>district" VALUE="<% $object->district %>">
 %   }
 % }
 
@@ -239,7 +240,7 @@ Example:
 %# keep a clean copy of the address so we know if we need
 %# to re-standardize
 % foreach (qw(address1 city state country zip latitude
-%             longitude censustract addr_clean) ) {
+%             longitude censustract district addr_clean) ) {
 <INPUT TYPE="hidden" NAME="old_<%$pre.$_%>" ID="old_<%$pre.$_%>" VALUE="<% $object->get($_) |h%>">
 % }
 %# Placeholders
index 4e61096..5689b12 100644 (file)
@@ -194,7 +194,7 @@ foreach my $svcdb ( FS::part_svc->svc_tables() ) {
   } elsif ( $svcdb eq 'svc_phone' ) {
 
     $report_svc{"${name}' total usage by time period"} = 
-      [ $fsurl. 'search/report_svc_phone.html',
+      [ $fsurl. 'search/report_svc_phone_usage.html',
         'Total usage (minutes, and amount billed) for the specified time period, per phone number.',
       ];
 
@@ -209,7 +209,7 @@ foreach my $svcdb ( FS::part_svc->svc_tables() ) {
 
     $report_svc{"Advanced $lcsname reports"} = 
         [ $fsurl."search/report_$svcdb.html", '' ]
-      if $svcdb =~ /^svc_(acct|broadband|hardware)$/
+      if $svcdb =~ /^svc_(acct|broadband|hardware|phone)$/
       && $curuser->access_right("Services: $name: Advanced search");
 
   if ( $svcdb eq 'svc_phone' ) {
@@ -236,7 +236,7 @@ tie my %report_packages, 'Tie::IxHash';
 $report_packages{'Package definitions (by # active)'} =  [ $fsurl.'browse/part_pkg.cgi?active=1', 'Package definitions by number of active packages' ]
   if    $curuser->access_right('Edit package definitions')
      || $curuser->access_right('Edit global package definitions');
-$report_packages{'Package Costs Report'} = [ $fsurl.'graph/report_cust_pkg_cost.html', 'Package setup and recurring costs graph' ]
+$report_packages{'Package costs'} = [ $fsurl.'graph/report_cust_pkg_cost.html', 'Package setup and recurring costs graph' ]
   if $curuser->access_right('Financial reports');
 $report_packages{'separator'} =  ''
   if keys %report_packages;
@@ -294,9 +294,11 @@ tie my %report_ticketing, 'Tie::IxHash',
   'Advanced ticket reports' => [ $fsurl.'rt/Search/Build.html?NewQuery=1', 'List tickets by any criteria' ],
 ;
 
-tie my %report_employees, 'Tie::IxHash',
-  'Employee Commission Report' => [ $fsurl.'search/report_employee_commission.html', '' ],
-  'Employee Audit Report' => [ $fsurl.'search/report_employee_audit.html', 'Employee audit report' ],
+tie my %report_employees, 'Tie::IxHash';
+$report_employees{'Employee Commission Report'} = [ $fsurl.'search/report_employee_commission.html', '' ]
+  if $curuser->access_right('Employees: Commission Report');
+$report_employees{'Employee Audit Report'} = [ $fsurl.'search/report_employee_audit.html', 'Employee audit report' ]
+  if $curuser->access_right('Employees: Audit Report');
 ;
 
 tie my %report_bill_event, 'Tie::IxHash',
@@ -336,7 +338,8 @@ tie my %report_sales, 'Tie::IxHash',
   'Daily Sales, Credits and Receipts' => [ $fsurl.'graph/report_money_time_daily.html', 'Sales, credits and receipts (broken down by day) summary graph' ],
   'Sales Report' => [ $fsurl.'graph/report_cust_bill_pkg.html', 'Sales report and graph (by agent, package class and/or date range)' ],
   'Rated Call Sales Report' => [ $fsurl.'graph/report_cust_bill_pkg_detail.html', 'Sales report and graph (by agent, package class, usage class and/or date range)' ],
-  'Sales With Advertising Source' => [ $fsurl.'search/report_cust_bill_pkg_referral.html' ],
+  'Sales with Advertising Source' => [ $fsurl.'search/report_cust_bill_pkg_referral.html' ],
+  'Sales with Agent Commissions' => [ $fsurl.'search/report_agent_commission.html' ],
 ;
 
 tie my %report_financial, 'Tie::IxHash';
@@ -396,7 +399,7 @@ $report_menu{'Tickets'}        = [ \%report_ticketing, 'Ticket reports' ]
   if $conf->config('ticket_system')
   ;#&& FS::TicketSystem->access_right(\%session, 'Something');
 $report_menu{'Employees'}      =  [ \%report_employees, 'Employee reports'  ]
-  if $curuser->access_right('Financial reports');
+  if keys %report_employees;
 $report_menu{'Billing events'} =  [ \%report_bill_event, 'Billing events' ]
   if $curuser->access_right('Billing event reports');
 $report_menu{'Financial'}      = [ \%report_financial, 'Financial reports' ]
@@ -463,6 +466,8 @@ $tools_menu{'Job Queue'} =  [ $fsurl.'search/queue.html', 'View pending job queu
   if $curuser->access_right('Job queue');
 $tools_menu{'Ticketing'} = [ \%tools_ticketing, 'Ticketing tools' ]
   if $conf->config('ticket_system');
+$tools_menu{'Customer email settings'} = [ $fsurl.'misc/manage_cust_email.html' ]
+  if $curuser->access_right('Edit customer');
 $tools_menu{'Business card scan'} = [ $fsurl.'edit/prospect_main-upload.html' ]
   if $curuser->access_right('New prospect');
 $tools_menu{'Time Queue'} =  [ $fsurl.'search/report_timeworked.html', 'View pending support time' ]
@@ -649,7 +654,7 @@ $config_misc{'Advertising sources'} = [ $fsurl.'browse/part_referral.html', 'Whe
   || $curuser->access_right('Edit global advertising sources');
 if ( $curuser->access_right('Configuration') ) {
   $config_misc{'Custom fields'} = [ $fsurl.'browse/part_virtual_field.html', 'Locally defined fields', ];
-  $config_misc{'Message catalog'} = [ $fsurl.'browse/msgcat.html', 'Change error messages and other customizable labels for each locale' ];
+  $config_misc{'Translation strings'} = [ $fsurl.'browse/msgcat.html', 'Translations and other customizable labels for each locale' ];
 }
 $config_misc{'Inventory classes and inventory'} = [ $fsurl.'browse/inventory_class.html', 'Setup inventory classes and stock inventory' ]
   if $curuser->access_right('Edit inventory')
index 8c1efd9..1069a0e 100644 (file)
@@ -44,4 +44,5 @@ function standardize_new_location() {
 
 function submit_abort() {
   document.OrderPkgForm.submitButton.disabled = false;
+  nd(1);
 }
index 7a282a3..cef54b8 100644 (file)
@@ -108,7 +108,7 @@ function <%$key%>process () {
 
 function <%$key%>myCallback( jobnum ) {
 
-  overlib( OLiframeContent('<%$p%>elements/progress-popup.html?jobnum=' + jobnum + ';<%$url_or_message_link%>;formname=<%$formname%>' , 444, 168, '<% $popup_name %>'), CAPTION, 'Please wait...', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', CLOSECLICK, MIDX, 0, MIDY, 0 );
+  overlib( OLiframeContent('<%$fsurl%>elements/progress-popup.html?jobnum=' + jobnum + ';<%$url_or_message_link%>;formname=<%$formname%>' , 444, 168, '<% $popup_name %>'), CAPTION, 'Please wait...', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', CLOSECLICK, MIDX, 0, MIDY, 0 );
 
 }
 
diff --git a/httemplate/elements/random_pass.html b/httemplate/elements/random_pass.html
new file mode 100644 (file)
index 0000000..b215b77
--- /dev/null
@@ -0,0 +1,17 @@
+<INPUT TYPE="button" VALUE="<% emt($label) %>" onclick="randomPass()">
+<SCRIPT TYPE="text/javascript">
+function randomPass() {
+  var i=0;
+  var pw_set='<% join('', 'a'..'z', 'A'..'Z', '0'..'9' ) %>';
+  var pass='';
+  while(i < 8) {
+    i++;
+    pass += pw_set.charAt(Math.floor(Math.random() * pw_set.length));
+  }
+  document.getElementById('<% $id %>').value = pass;
+}
+</SCRIPT>
+<%init>
+my $id = shift;
+my $label = shift || 'Generate';
+</%init>
diff --git a/httemplate/elements/search-svc_broadband.html b/httemplate/elements/search-svc_broadband.html
new file mode 100644 (file)
index 0000000..d835161
--- /dev/null
@@ -0,0 +1,204 @@
+<%doc>
+
+Example:
+
+  include( '/elements/search-svc_broadband.html,
+             'field'       => 'svcnum',
+             #slightly deprecated old synonym for field#'field_name'=>'svcnum',
+             'find_button' => 1, #add a "find" button to the field
+             'curr_value'  => 54, #current value
+             'value        => 32, #deprecated synonym for curr_value
+  );
+
+</%doc>
+<INPUT TYPE="hidden" NAME="<% $field %>" ID="<% $field %>" VALUE="<% $value %>">
+
+<!-- some false laziness w/ misc/batch-cust_pay.html, though not as bad as i'd thought at first... -->
+
+<INPUT TYPE = "text"
+       NAME = "<% $field %>_search"
+       ID   = "<% $field %>_search"
+       SIZE = "32"
+       VALUE="<% $svc_broadband ? $svc_broadband->label : '(svcnum, ip or mac)' %>"
+       onFocus="clearhint_<% $field %>_search(this);"
+       onClick="clearhint_<% $field %>_search(this);"
+       onChange="smart_<% $field %>_search(this);"
+>
+
+% if ( $opt{'find_button'} ) {
+    <INPUT TYPE    = "button"
+           VALUE   = 'Find',
+           NAME    = "<% $field %>_findbutton"
+           onClick = "smart_<% $field %>_search(this.form.<% $field %>_search);"
+    >
+% }
+
+<SELECT NAME="<% $field %>_select" ID="<% $field %>_select" STYLE="color:#ff0000; display:none" onChange="select_<% $field %>(this);">
+</SELECT>
+
+<% include('/elements/xmlhttp.html',
+              'url'  => $p. 'misc/xmlhttp-svc_broadband-search.cgi',
+              'subs' => [ 'smart_search' ],
+           )
+%>
+
+<SCRIPT TYPE="text/javascript">
+
+  function clearhint_<% $field %>_search (what) {
+
+    what.style.color = '#000000';
+
+    if ( what.value == '(svcnum, ip or mac)' )
+      what.value = '';
+
+    if ( what.value.indexOf('Service not found: ') == 0 )
+      what.value = what.value.substr(20);
+
+  }
+
+  var <% $field %>_search_active = false;
+
+  function smart_<% $field %>_search(what) {
+
+    if ( <% $field %>_search_active )
+      return;
+
+    var service = what.value;
+
+    if ( service == 'searching...' || service == ''
+         || service.indexOf('Service not found: ') == 0 )
+      return;
+
+    if ( what.getAttribute('magic') == 'nosearch' ) {
+      what.setAttribute('magic', '');
+      return;
+    }
+
+    //what.value = 'searching...'
+    what.disabled = true;
+    what.style.color= '#000000';
+    what.style.backgroundColor = '#dddddd';
+
+    var service_select = document.getElementById('<% $field %>_select');
+
+    //alert("search for customer " + customer);
+
+    function <% $field %>_search_update(services) {
+
+      //alert('customers returned: ' + customers);
+
+      var serviceArray = eval('(' + services + ')');
+
+      what.disabled = false;
+      what.style.backgroundColor = '#ffffff';
+
+      if ( serviceArray.length == 0 ) {
+
+        what.form.<% $field %>.value = '';
+
+        what.value = 'Service not found: ' + what.value;
+        what.style.color = '#ff0000';
+
+        what.style.display = '';
+        service_select.style.display = 'none';
+
+      } else if ( serviceArray.length == 1 ) {
+
+        //alert('one customer found: ' + customerArray[0]);
+
+        what.form.<% $field %>.value = serviceArray[0][0];
+        what.value = serviceArray[0][1];
+
+        what.style.display = '';
+        service_select.style.display = 'none';
+
+      } else {
+
+        //alert('multiple customers found, have to create select dropdown');
+
+        //blank the current list
+        for ( var i = service_select.length; i >= 0; i-- )
+          service_select.options[i] = null;
+
+        opt(service_select, '', 'Multiple services match "' + service + '" - select one', '#ff0000');
+
+        //add the multiple services
+        for ( var s = 0; s < serviceArray.length; s++ )
+          opt(service_select, serviceArray[s][0], serviceArray[s][1], '#000000');
+
+        opt(service_select, 'cancel', '(Edit search string)', '#000000');
+
+        what.style.display = 'none';
+        service_select.style.display = '';
+
+      }
+
+      <% $field %>_search_active = false;
+
+    }
+
+    <% $field %>_search_active = true;
+
+    smart_search( service, <% $field %>_search_update );
+
+
+  }
+
+  function select_<% $field %> (what) {
+
+    var svcnum = what.options[what.selectedIndex].value;
+    var service = what.options[what.selectedIndex].text;
+
+    var service_obj = document.getElementById('<% $field %>_search');
+
+    if ( svcnum == '' ) {
+      //what.style.color = '#ff0000';
+
+    } else if ( svcnum == 'cancel' ) {
+
+      service_obj.style.color = '#000000';
+
+      what.style.display = 'none';
+      service_obj.style.display = '';
+      service_obj.focus();
+
+    } else {
+    
+      what.form.<% $field %>.value = svcnum;
+
+      service_obj.value = service;
+      service_obj.style.color = '#000000';
+
+      what.style.display = 'none';
+      service_obj.style.display = '';
+
+    }
+
+  }
+
+  function opt(what,value,text,color) {
+    var optionName = new Option(text, value, false, false);
+    optionName.style.color = color;
+    var length = what.length;
+    what.options[length] = optionName;
+  }
+
+</SCRIPT>
+<%init>
+
+my( %opt ) = @_;
+
+my $field = $opt{'field'} || $opt{'field_name'} || 'svcnum';
+
+my $value = $opt{'curr_value'} || $opt{'value'};
+
+my $svc_broadband = '';
+if ( $value ) {
+  $svc_broadband = qsearchs({
+    'table'     => 'svc_broadband',
+    'hashref'   => { 'svcnum' => $value },
+    #have to join to cust_main for an agentnum 'extra_sql' => " AND ". $FS::CurrentUser::CurrentUser->agentnums_sql,
+  });
+}
+
+</%init>
index a302bef..f0f56d5 100644 (file)
@@ -17,7 +17,7 @@
     what.form.<% $opt{'prefix'} %>areacode.disabled = 'disabled';
     what.form.<% $opt{'prefix'} %>areacode.style.display = 'none';
     var areacodewait = document.getElementById('<% $opt{'prefix'} %>areacodewait');
-    areacodewait.style.display = '';
+    areacodewait.style.display = 'inline';
     var areacodeerror = document.getElementById('<% $opt{'prefix'} %>areacodeerror');
     areacodeerror.style.display = 'none';
 
@@ -61,7 +61,7 @@
         what.form.<% $opt{'prefix'} %>areacode.style.display = '';
       } else {
         var areacodeerror = document.getElementById('<% $opt{'prefix'} %>areacodeerror');
-        areacodeerror.style.display = '';
+        areacodeerror.style.display = 'inline';
       }
 
       //run the callback
index 6e205d8..c396031 100644 (file)
@@ -18,6 +18,28 @@ Example:
     <TABLE>
       <TR>
 
+%       my( $phonenum_checked, $manual_checked ) = ( '', '' );
+%       if ( $export->get_dids_can_manual ) {
+%         #not 100% perfect UI on error handling, but it'll do
+%         if ( $opt{'curr_value'} ) {
+%           $phonenum_checked = '';
+%           $manual_checked   = 'CHECKED';
+%         } else {
+%           $phonenum_checked = 'CHECKED';
+%           $manual_checked   = '';
+%         }
+
+        <TD VALIGN="top">
+          <INPUT TYPE     = "radio"
+                 NAME     = "phonenum_which"
+                 VALUE    = "phonenum"
+                 onChange = "phonenum_which_changed(this)"
+                 onClick  = "phonenum_which_changed(this)"
+                 <% $phonenum_checked %>
+          > Inventory
+        </TD>
+%       }
+
 %       if ( $export->get_dids_npa_select ) {
 
         <TD VALIGN="top">
@@ -27,9 +49,10 @@ Example:
                        'svcpart'       => $svcpart,
                        'disable_empty' => 0,
                        'empty_label'   => 'Select state',
+                       'disabled'      => ( $manual_checked ? 1 : 0 ),
                     )
           %>
-          <BR><FONT SIZE="-1">State</FONT>
+          <BR><FONT SIZE="-1" ID="phonenum_state_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>State</FONT>
         </TD>
 
           <TD VALIGN="top">
@@ -39,19 +62,24 @@ Example:
                          'empty'        => 'Select area code',
                       )
             %>
-            <BR><FONT SIZE="-1">Area code</FONT>
+            <BR><FONT SIZE="-1" ID="areacode_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Area code</FONT>
           </TD>
 
           <TD VALIGN="top">
             <% include('/elements/select-exchange.html',
-                         'svcpart' => $svcpart,
-                         'empty'   => 'Select exchange',
+                         'svcpart'  => $svcpart,
+                         'empty'    => 'Select exchange',
                       )
             %>
-            <BR><FONT SIZE="-1">City / Exchange</FONT>
+            <BR><FONT SIZE="-1" ID="exchange_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>City / Exchange</FONT>
           </TD>
 
 %       } else {
+%
+%       #this code path currently only being used by fibernetics
+%       # should change "Province" label to "State" or make it configurable
+%       # if/when other folks need an areacode-less DID selector that goes
+%       # directly from state to region
 
         <TD VALIGN="top">
           <% include('/elements/select.html',
@@ -60,9 +88,10 @@ Example:
                        'options'  => [ '', @{ $export->get_dids } ],
                        'labels'   => { '' => 'Select province' },
                        'onchange' => 'phonenum_state_changed(this);',
+                       'disabled' => ( $manual_checked ? 1 : 0 ),
                     )
           %>
-          <BR><FONT SIZE="-1">Province</FONT>
+          <BR><FONT SIZE="-1" ID="phonenum_state_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Province</FONT>
         </TD>
 
           <TD VALIGN="top">
@@ -72,7 +101,7 @@ Example:
                          'empty'         => 'Select region',
                       )
             %>
-            <BR><FONT SIZE="-1">Region</FONT>
+            <BR><FONT SIZE="-1" ID="region_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Region</FONT>
           </TD>
 
 %       }
@@ -86,10 +115,132 @@ Example:
                        'region'   => ! $export->get_dids_npa_select,
                     )
           %>
-          <BR><FONT SIZE="-1">Phone number</FONT>
+          <BR><FONT SIZE="-1" ID="phonenum_phonenum_label" <% $manual_checked ? 'STYLE="color:#999999"' : '' %>>Phone number</FONT>
         </TD>
 
       </TR>
+
+%       if ( $export->get_dids_can_manual ) {
+          <TR>
+
+            <TD VALIGN="top">
+              <INPUT TYPE     = "radio"
+                     NAME     = "phonenum_which"
+                     VALUE    = "phonenum_manual"
+                     onChange = "phonenum_which_changed(this)"
+                     onClick  = "phonenum_which_changed(this)"
+                     <% $manual_checked %>
+              > Manual entry
+            </TD>
+
+            <TD VALIGN="top" COLSPAN=4>
+              <& /elements/input-text.html,
+                   %opt,
+                   field    => 'phonenum_manual',
+                   id       => 'phonenum_manual',
+                   type     => 'text',
+                   disabled => ( $phonenum_checked ? 1 : 0 ),
+              &>
+            </TD>
+          </TR>
+
+          <SCRIPT TYPE="text/javascript">
+            function phonenum_which_changed(what) {
+
+              if ( what.value == 'phonenum' && what.checked ) {
+
+                what.form.phonenum_manual.disabled = true;
+                what.form.phonenum_manual.style.backgroundColor = '#dddddd';
+
+                what.form.phonenum_state.disabled = false;
+
+                document.getElementById('phonenum_state_label').style.color = '#000000';
+                if ( document.getElementById('areacode_label') ) {
+                  document.getElementById('areacode_label').style.color = '#000000';
+                }
+                if ( document.getElementById('exchange_label') ) {
+                  document.getElementById('exchange_label').style.color = '#000000';
+                }
+                if ( document.getElementById('region_label') ) {
+                  document.getElementById('region_label').style.color = '#000000';
+                }
+                document.getElementById('phonenum_phonenum_label').style.color = '#000000';
+
+                var value = what.form.phonenum_state.options[ what.form.phonenum_state.selectedIndex].value;
+
+                if ( value != '' ) {
+
+                  if ( what.form.areacode ) {
+                    what.form.areacode.disabled = false;
+
+                    var areacode_value = what.form.areacode.options[ what.form.areacode.selectedIndex].value;
+
+                    if ( areacode_value != '' ) {
+                      what.form.exchange.disabled = false;
+
+                      var exchange_value = what.form.exchange.options[ what.form.exchange.selectedIndex].value;
+
+                      if ( exchange_value != '' ) {
+                        what.form.phonenum.disabled = false;
+                      }
+
+                    }
+
+                  }
+                  if ( what.form.region ) {
+                    what.form.region.disabled = false;
+                    
+                    var region_value = what.form.region.options[ what.form.region.selectedIndex].value;
+
+                    if ( region_value != '' ) {
+                      what.form.phonenum.disabled = false;
+                    }
+
+                  }
+
+                }
+
+              }
+
+              if ( what.value == 'phonenum_manual' && what.checked ) {
+
+                what.form.phonenum_manual.disabled = false;
+                what.form.phonenum_manual.style.backgroundColor = '#ffffff';
+
+                what.form.phonenum_state.disabled = true;
+
+                document.getElementById('phonenum_state_label').style.color = '#999999';
+                if ( document.getElementById('areacode_label') ) {
+                  document.getElementById('areacode_label').style.color = '#999999';
+                }
+                if ( document.getElementById('exchange_label') ) {
+                  document.getElementById('exchange_label').style.color = '#999999';
+                }
+                if ( document.getElementById('region_label') ) {
+                  document.getElementById('region_label').style.color = '#999999';
+                }
+                document.getElementById('phonenum_phonenum_label').style.color = '#999999';
+
+                if ( what.form.areacode ) {
+                  what.form.areacode.disabled = true;
+                }
+
+                if ( what.form.exchange ) {
+                  what.form.exchange.disabled = true;
+                }
+
+                if ( what.form.region ) {
+                  what.form.region.disabled = true;
+                }
+
+                what.form.phonenum.disabled = true;
+              }
+
+            }
+          </SCRIPT>
+
+%       }
+
     </TABLE>
 
 % } 
index 9e4b5ce..b967709 100644 (file)
@@ -17,7 +17,7 @@
     what.form.<% $opt{'prefix'} %>exchange.disabled = 'disabled';
     what.form.<% $opt{'prefix'} %>exchange.style.display = 'none';
     var exchangewait = document.getElementById('<% $opt{'prefix'} %>exchangewait');
-    exchangewait.style.display = '';
+    exchangewait.style.display = 'inline';
     var exchangeerror = document.getElementById('<% $opt{'prefix'} %>exchangeerror');
     exchangeerror.style.display = 'none';
 
@@ -56,7 +56,7 @@
         what.form.<% $opt{'prefix'} %>exchange.style.display = '';
       } else {
         var exchangeerror = document.getElementById('<% $opt{'prefix'} %>exchangeerror');
-        exchangeerror.style.display = '';
+        exchangeerror.style.display = 'inline';
       }
 
       //run the callback
index 8b1c71f..4b406fc 100644 (file)
@@ -7,7 +7,7 @@
 <% include( '/elements/input-text.html', %opt, 'type'=>'text' ) %>
 
 <SELECT ID="<% $opt{'prefix'} %>sel_mac_addr" NAME="<% $opt{'prefix'} %>sel_mac_addr" 
-    notonChange="<% $opt{'prefix'} %>mac_addr_changed(this); <% $opt{'onchange'} %>"
+%#    notonChange="<% $opt{'prefix'} %>mac_addr_changed(this); <% $opt{'onchange'} %>"
     <% $opt{'disabled'} %> STYLE="display: none">
   <OPTION VALUE="">Select MAC address</OPTION>
 </SELECT>
index 72ab7f6..743b285 100644 (file)
@@ -13,6 +13,9 @@ my( %opt ) = @_;
 $opt{'records'} = delete $opt{'part_svc'}
   if $opt{'part_svc'};
 
-$opt{'records'} ||= [ qsearch( 'part_svc', {} ) ]; # { disabled=>'' } )
+my %hash = ();
+$hash{'svcdb'} = $opt{'svcdb'} if $opt{'svcdb'};
+
+$opt{'records'} ||= [ qsearch( 'part_svc', \%hash ) ]; # { disabled=>'' } )
 
 </%init>
index 18abe3d..a8d9a7c 100644 (file)
@@ -17,7 +17,7 @@
     what.form.<% $opt{'prefix'} %>phonenum.disabled = 'disabled';
     what.form.<% $opt{'prefix'} %>phonenum.style.display = 'none';
     var phonenumwait = document.getElementById('<% $opt{'prefix'} %>phonenumwait');
-    phonenumwait.style.display = '';
+    phonenumwait.style.display = 'inline';
     var phonenumerror = document.getElementById('<% $opt{'prefix'} %>phonenumerror');
     phonenumerror.style.display = 'none';
 
@@ -54,7 +54,7 @@
         what.form.<% $opt{'prefix'} %>phonenum.style.display = '';
       } else {
         var phonenumerror = document.getElementById('<% $opt{'prefix'} %>phonenumerror');
-        phonenumerror.style.display = '';
+        phonenumerror.style.display = 'inline';
       }
 
       //run the callback
index 9823290..7ed9592 100644 (file)
@@ -17,7 +17,7 @@
     what.form.<% $opt{'prefix'} %>region.disabled = 'disabled';
     what.form.<% $opt{'prefix'} %>region.style.display = 'none';
     var regionwait = document.getElementById('<% $opt{'prefix'} %>regionwait');
-    regionwait.style.display = '';
+    regionwait.style.display = 'inline';
     var regionerror = document.getElementById('<% $opt{'prefix'} %>regionerror');
     regionerror.style.display = 'none';
 
@@ -56,7 +56,7 @@
         what.form.<% $opt{'prefix'} %>region.style.display = '';
       } else {
         var regionerror = document.getElementById('<% $opt{'prefix'} %>regionerror');
-        regionerror.style.display = '';
+        regionerror.style.display = 'inline';
       }
 
       //run the callback
index c0cd7a5..b6c1573 100644 (file)
@@ -8,7 +8,7 @@ Example:
     # required
     ##
     'table'          => 'table_name',
-    'name_col'       => 'name_column',
+    'name_col'       => 'name_column', #or method if you pass an order_by
    
     #strongly recommended (you want your forms to be "sticky" on errors, right?)
     'curr_value'     => 'current_value',
@@ -111,6 +111,7 @@ Example:
      <% $opt{'label_callback'}
           ? &{ $opt{'label_callback'} }( $record )
           : $record->$name_col()
+        |h
      %>
 % } 
 
index e332eef..3ff5471 100644 (file)
@@ -124,13 +124,6 @@ my %opt = @_;
 my $pre = $opt{prefix} || '';
 my $tiers = $opt{tiers} or die "no tiers defined";
 
-#my $json = JSON->new()->canonical(); #sort
-# something super weird and broken going on with JSON's auto-loading, just
-# using JSON alone errors out with
-#   Can't locate object method "new" via package "null" (perhaps you forgot to
-#   load "null"?)
-# yes, "null", not "JSON".  so instead, using JSON::XS explicity...
-use JSON::XS;
 my $json = JSON::XS->new();
 $json->canonical;
 
index 01fd590..cb1d2d6 100644 (file)
@@ -236,7 +236,7 @@ sub layer_callback {
       $date_noinit = 1;
     }
     else {
-      $include = "input-$include" if $include =~ /^(text|money)$/;
+      $include = "input-$include" if $include =~ /^(text|money|percentage)$/;
       $include = "tr-$include" unless $include eq 'hidden';
       $html .= include( "/elements/$include.html",
                         %$lf,
index 15c5761..e98039d 100644 (file)
@@ -7,8 +7,8 @@ function status_message(text, caption) {
 function form_address_info() {
   var cf = document.<% $formname %>;
 
-  var returnobj = { onlyship: <% $onlyship ? 1 : 0 %> };
-% if ( !$onlyship ) {
+  var returnobj = { billship: <% $billship %> };
+% if ( $billship ) {
   returnobj['same'] = cf.elements['same'].checked;
 % }
 % if ( $withfirm ) {
@@ -59,16 +59,12 @@ function standardize_locations() {
     cf.elements['<% $pre %>coord_auto'].value = 'Y';
     changed = true;
   }
-
-% } #foreach $pre
-
   // standardize if the old address wasn't clean
-  if ( cf.elements['old_ship_addr_clean'].value == '' ||
-       cf.elements['old_bill_addr_clean'].value == '' ) {
-
+  if ( cf.elements['<% $pre %>addr_clean'].value == '' ) {
     changed = true;
-
   }
+% } #foreach $pre
+
   // or if it was clean but has been changed
   for (var key in address_info) {
     var old_el = cf.elements['old_'+key];
@@ -81,7 +77,7 @@ function standardize_locations() {
 % # If address hasn't been changed, auto-confirm the existing value of 
 % # censustract so that we don't ask the user to confirm it again.
 
-  if ( !changed ) {
+  if ( !changed && <% $withcensus %> ) {
     if ( address_info['same'] ) {
       cf.elements['bill_censustract'].value =
         address_info['bill_censustract'];
@@ -195,12 +191,14 @@ function post_standardization() {
 
 % if ( $conf->exists('enable_taxproducts') ) {
 
+  var cf = document.<% $formname %>;
+
   if ( new String(cf.elements['<% $taxpre %>zip'].value).length < 10 )
   {
 
     var country_el = cf.elements['<% $taxpre %>country'];
     var country = country_el.options[ country_el.selectedIndex ].value;
-    var geocode = cf.elements['geocode'].value;
+    var geocode = cf.elements['bill_geocode'].value;
 
     if ( country == 'CA' || country == 'US' ) {
 
@@ -222,14 +220,14 @@ function post_standardization() {
 
     } else {
 
-      cf.elements['geocode'].value = 'DEFAULT';
+      cf.elements['bill_geocode'].value = 'DEFAULT';
       <% $post_geocode %>;
 
     }
 
   } else {
 
-    cf.elements['geocode'].value = '';
+    cf.elements['bill_geocode'].value = '';
     <% $post_geocode %>;
 
   }
@@ -254,14 +252,14 @@ function update_geocode() {
     cf.elements['<% $taxpre %>city'].value     = argsHash['city'];
     setselect(cf.elements['<% $taxpre %>state'], argsHash['state']);
     cf.elements['<% $taxpre %>zip'].value      = argsHash['zip'];
-    cf.elements['geocode'].value  = argsHash['geocode'];
+    cf.elements['bill_geocode'].value  = argsHash['geocode'];
     <% $post_geocode %>;
 
   }
 
   // popup a chooser
 
-  overlib( OLresponseAJAX, CAPTION, 'Select tax location', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, DRAGGABLE, WIDTH, 576, HEIGHT, 268, BGCOLOR, '#333399', CGCOLOR, '#333399', TEXTSIZE, 3 );
+  overlib( OLresponseAJAX, CAPTION, 'Select tax location', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, WIDTH, 576, HEIGHT, 268, BGCOLOR, '#333399', CGCOLOR, '#333399', TEXTSIZE, 3 );
 
 }
 
@@ -279,21 +277,18 @@ function setselect(el, value) {
 my %opt = @_;
 my $conf = new FS::Conf;
 
-my $withfirm = 1;
-my $withcensus = 1;
+my $withfirm = $opt{'with_firm'} ? 1 : 0;
+my $withcensus = $opt{'with_census'} ? 1 : 0;
+
+my @prefixes = '';
+my $billship = $opt{'billship'} ? 1 : 0; # whether to have bill_ and ship_ prefixes
+my $taxpre = '';
+if ($billship) {
+  @prefixes = qw(bill_ ship_);
+  $taxpre = $conf->exists('tax-ship_address') ? 'ship_' : 'bill_';
+}
 
 my $formname =  $opt{form} || 'CustomerForm';
-my $onlyship =  $opt{onlyship} || '';
-#my $main_prefix =  $opt{main_prefix} || '';
-#my $ship_prefix =  $opt{ship_prefix} || ($onlyship ? '' : 'ship_');
-# The prefixes are now 'ship_' and 'bill_'.
-my $taxpre = 'bill_';
-$taxpre = 'ship_' if ( $conf->exists('tax-ship_address') || $onlyship );
 my $post_geocode = $opt{callback} || 'post_geocode();';
-$withfirm = 0 if $opt{no_company};
-$withcensus = 0 if $opt{no_census};
-
-my @prefixes = ('ship_');
-unshift @prefixes, 'bill_' unless $onlyship;
 
 </%init>
index 1ca22f6..b66654f 100644 (file)
@@ -96,7 +96,8 @@ my $svc_unprovision_link =
 my $manage_link = $opt{'manage_link'};
 my $manage_target = '';
 if ( $part_svc->svcdb eq 'svc_broadband' and $manage_link ) {
-  my $ip_addr = $svc_x->ip_addr; #substitution for $manage_link
+  my $ip_addr  = $svc_x->ip_addr;  #substitution for $manage_link
+  my $mac_addr = $svc_x->mac_addr; # ditto
   $manage_link = eval(qq("$manage_link"));
   $opt{'manage_link_text'} ||= mt('Manage Device');
   $opt{'manage_link_loc'}  ||= 'bottom';
index 7481c9b..ffc9038 100644 (file)
@@ -74,7 +74,7 @@ my( $input_time, $time_format, $time_hint ) = ( '', '', '' );
 my( $size, $maxlength ) = ( 11, 10 );
 if ( $opt{'input_time'} ) {
   $input_time  = ', showsTime: true, timeFormat: "12"'; # http://www.dynarch.com/demos/jscalendar/doc/html/reference.html#node_sec_2.3
-  $time_format = ' %k:%M:%S'; # http://www.dynarch.com/demos/jscalendar/doc/html/reference.html#node_sec_5.3.5
+  $time_format = ' %H:%M:%S'; # http://www.dynarch.com/demos/jscalendar/doc/html/reference.html#node_sec_5.3.5
   $time_hint   = ' h:m:s';
   $size = 21;
   $maxlength = 27;
diff --git a/httemplate/elements/tr-search-svc_broadband.html b/httemplate/elements/tr-search-svc_broadband.html
new file mode 100644 (file)
index 0000000..cd7c115
--- /dev/null
@@ -0,0 +1,15 @@
+<& tr-td-label.html, @_  &>
+
+  <TD <% $colspan %> <% $cell_style %> ID="<% $opt{input_id} || $opt{id}.'_input0' %>"><& search-svc_broadband.html, @_  &></TD>
+
+</TR>
+
+<%init>
+
+my %opt = @_;
+
+my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
+
+my $colspan = $opt{'colspan'} ? 'COLSPAN="'.$opt{'colspan'}.'"' : '';
+
+</%init>
diff --git a/httemplate/elements/tr-select-contact.html b/httemplate/elements/tr-select-contact.html
new file mode 100644 (file)
index 0000000..d6bc67f
--- /dev/null
@@ -0,0 +1,204 @@
+<%doc>
+
+Example:
+
+  include('/elements/tr-select-contact.html',
+            'cgi'       => $cgi,
+
+            'cust_main'     => $cust_main,
+            #or
+            'prospect_main' => $prospect_main,
+
+            #optional
+            'empty_label'   => '(default contact)',
+         )
+
+</%doc>
+
+<SCRIPT TYPE="text/javascript">
+
+  function contact_disable(what) {
+%   for (@contact_fields) { 
+      what.form.<%$_%>.disabled = true;
+      var ftype = what.form.<%$_%>.tagName;
+      if( ftype == 'SELECT') changeSelect(what.form.<%$_%>, '');
+      else what.form.<%$_%>.value = '';
+      if( ftype != 'SELECT') what.form.<%$_%>.style.backgroundColor = '#dddddd';
+%   } 
+  }
+
+  function contact_clear(what) {
+%   for (@contact_fields) { 
+      var ftype = what.form.<%$_%>.tagName;
+      if( ftype == 'INPUT' ) what.form.<%$_%>.value = '';
+%   }
+  }
+
+  function contact_enable(what) {
+%   for (@contact_fields) { 
+      what.form.<%$_%>.disabled = false;
+      var ftype = what.form.<%$_%>.tagName;
+      if( ftype != 'SELECT') what.form.<%$_%>.style.backgroundColor = '#ffffff';
+%   } 
+  }
+
+  function contactnum_changed(what) {
+    var contactnum = what.options[what.selectedIndex].value;
+    if ( contactnum == -1 ) { //Add new contact
+      contact_clear(what);
+
+      contact_enable(what);
+      return;
+    }
+
+%   if ( $editable ) {
+      if ( contactnum == 0 ) {
+%   }
+
+%       #sleep/wait until dropdowns are updated?
+        contact_disable(what);
+
+%   if ( $editable ) {
+      } else {
+
+%       #sleep/wait until dropdowns are updated?
+        contact_enable(what);
+
+      }
+%   }
+
+  }
+
+  function changeSelect(what, value) {
+    for ( var i=0; i<what.length; i++) {
+      if ( what.options[i].value == value ) {
+        what.selectedIndex = i;
+      }
+    }
+  }
+
+</SCRIPT>
+
+<TR>
+  <<%$th%> ALIGN="right" VALIGN="top"><% $opt{'label'} || emt('Service contact') %></<%$th%>>
+  <TD VALIGN="top" COLSPAN=7>
+    <SELECT NAME     = "contactnum"
+            ID       = "contactnum"
+            STYLE    = "vertical-align:top;margin:3px"
+            onchange = "contactnum_changed(this);"
+    >
+% if ( $cust_main ) {
+      <OPTION VALUE=""><% $opt{'empty_label'} || '(customer default)' |h %>
+% }
+%
+%     foreach my $contact ( @contact ) {
+        <OPTION VALUE="<% $contact->contactnum %>"
+                <% $contactnum == $contact->contactnum ? 'SELECTED' : '' %>
+        ><% $contact->line |h %>
+%     }
+%     if ( $addnew ) {
+        <OPTION VALUE="-1"
+                <% $contactnum == -1 ? 'SELECTED' : '' %>
+        >New contact
+%     }
+    </SELECT>
+
+<% include('/elements/contact.html',
+             'object'       => $contact,
+             #'onchange' ?  probably not
+             'disabled'     => $disabled,
+             'name_only'    => 1,
+          )
+%>
+
+  </TD>
+</TR>
+
+<SCRIPT TYPE="text/javascript">
+  contactnum_changed(document.getElementById('contactnum'));
+</SCRIPT>
+<%init>
+
+#based on / kinda false laziness w/tr-select-cust_contact.html
+
+my $conf = new FS::Conf;
+
+my %opt = @_;
+my $cgi           = $opt{'cgi'};
+my $cust_pkg      = $opt{'cust_pkg'};
+my $cust_main     = $opt{'cust_main'};
+my $prospect_main = $opt{'prospect_main'};
+die "cust_main or prospect_main required" unless $cust_main or $prospect_main;
+
+my $contactnum = '';
+if ( $cgi->param('error') ) {
+  $cgi->param('contactnum') =~ /^(\-?\d*)$/ or die "illegal contactnum";
+  $contactnum = $1;
+} else {
+  if ( length($opt{'curr_value'}) ) {
+    $contactnum = $opt{'curr_value'};
+  } elsif ($prospect_main) {
+    my @cust_contact = $prospect_main->cust_contact;
+    $contactnum = $cust_contact[0]->contactnum if scalar(@cust_contact)==1;
+  } else { #$cust_main
+    $cgi->param('contactnum') =~ /^(\-?\d*)$/ or die "illegal contactnum";
+    $contactnum = $1;
+  }
+}
+
+##probably could use explicit controls
+#my $editable = $cust_main ? 0 : 1; #could use explicit control
+my $editable = 0;
+my $addnew = $cust_main ? 1 : ( $contactnum>0 ? 0 : 1 );
+
+my @contact_fields = map "contactnum_$_", qw( first last );
+
+my $contact; #the one that shows by default in the contact edit space
+if ( $contactnum && $contactnum > 0 ) {
+  $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
+    or die "unknown contactnum";
+} else {
+  $contact = new FS::contact;
+  if ( $contactnum == -1 ) {
+    $contact->$_( $cgi->param($_) ) foreach @contact_fields; #XXX
+  } elsif ( $cust_pkg && $cust_pkg->contactnum ) {
+    my $pkg_contact = $cust_pkg->contact_obj;
+    $contact->$_( $pkg_contact->$_ ) foreach @contact_fields; #XXX why are we making a new one gagain??
+    $opt{'empty_label'} ||= 'package contact: '.$pkg_contact->line;
+  } elsif ( $cust_main ) {
+    $contact = new FS::contact; #I think
+  }
+}
+
+my $contact_sort = sub {
+     lc($a->last)  cmp lc($b->last)
+  or lc($a->first) cmp lc($b->first)
+};
+
+my @contact;
+push @contact, $cust_main->cust_contact if $cust_main;
+push @contact, $prospect_main->contact if $prospect_main;
+push @contact, $contact
+  if !$cust_main && $contact && $contact->contactnum > 0
+  && ! grep { $_->contactnum == $contact->contactnum } @contact;
+
+@contact = sort $contact_sort grep !$_->disabled, @contact;
+
+$contact = $contact[0]
+  if ( $prospect_main )
+  && !$opt{'is_optional'}
+  && @contact;
+
+my $disabled =
+  ( $contactnum < 0
+    || ( $editable && $contactnum )
+    || ( $prospect_main
+         && !$opt{'is_optional'} && !@contact && $addnew
+       )
+  )
+    ? ''
+    : 'DISABLED';
+
+my $th = $opt{'no_bold'} ? 'TD' : 'TH';
+
+</%init>
index 7ffbd6c..780bf96 100644 (file)
@@ -153,25 +153,16 @@ Example:
     }
   }
 
+  var location_fields = <% encode_json(\@location_fields) %>;
   function update_location( string ) {
-    var hash = eval('('+string+')');
-    document.getElementById('address1').value = hash['address1'];
-    document.getElementById('city').value     = hash['city'];
-    document.getElementById('zip').value      = hash['zip'];
-
-%   if ( $opt{'alt_format'} ) {
-      changeSelect( document.getElementById('location_kind'), hash['location_kind']);
-      changeSelect( document.getElementById('location_type'), hash['location_type']);
-      document.getElementById('location_number').value = hash['location_number'];
-%   } else {
-      document.getElementById('address2').value = hash['address2'];
-%   }
-
-    var country_el = document.getElementById('country');
-
-    changeSelect( country_el, hash['country'] );
-
-    country_changed( country_el,
+    var hash = JSON.parse(string);
+    for(var i = 0; i < location_fields.length; i++) {
+      var f = location_fields[i];
+      if (hash[f] && document.getElementById(f))  {
+        document.getElementById(f).value = hash[f];
+      }
+    }
+    country_changed( document.getElementById('country'),
                      fix_state_factory( hash['state'],
                                         hash['county']
                                       )
@@ -185,7 +176,7 @@ Example:
   <TD COLSPAN=7>
     <SELECT NAME     = "locationnum"
             ID       = "locationnum"
-            onChange = "locationnum_changed(this);"
+            onchange = "locationnum_changed(this);"
     >
 % if ( $cust_main ) {
       <OPTION VALUE="<% $cust_main->ship_locationnum %>"><% $opt{'empty_label'} || '(default service address)' |h %>
@@ -258,9 +249,7 @@ if ( $cgi->param('error') ) {
 my $editable = $cust_main ? 0 : 1; #could use explicit control
 my $addnew = $cust_main ? 1 : ( $locationnum>0 ? 0 : 1 );
 
-my @location_fields = qw( address1 address2 city county state zip country
-                          latitude longitude
-                        );
+my @location_fields = FS::cust_main->location_fields;
 if ( $opt{'alt_format'} ) {
     push @location_fields, qw( location_type location_number location_kind );
 }
index 987ade6..2aa712f 100644 (file)
@@ -1,6 +1,6 @@
 <% include('tr-td-label.html', @_ ) %>
 
-% if ( $opt{'curr_value'} ne '' && $use_selector ) {
+% if ( $use_selector && $opt{'curr_value'} ne '' && ! $can_edit ) {
 
     <TD BGCOLOR="#dddddd" <% $cell_style %>><% $opt{'formatted_value'} || $opt{'curr_value'} || $opt{'value'} |h %></TD>
     
@@ -38,4 +38,6 @@ if ( scalar(@exports) > 1 ) {
 
 my $use_selector = scalar(@exports) ? 1 : 0;
 
+my $can_edit = scalar(@exports) && $exports[0]->get_dids_can_edit;
+
 </%init>
index e9faeb2..d4218f8 100644 (file)
@@ -24,7 +24,9 @@ function change_discount_term(what) {
         id      => 'discount_term',
         options => [ '', @discount_term ],
         labels  => { '' => mt('1 month'), 
-                     map { $_ => mt('[_1] months', $_) } @discount_term },
+                     map { $_ => mt('[_1] months', sprintf('%.0f', $_)) }
+                      @discount_term
+                   },
         curr_value => '',
         onchange => $amount_id ? 'change_discount_term(this)' : '',
       &>
index a27412f..ad9b40a 100644 (file)
@@ -39,7 +39,7 @@
   my %hash = (
     'show_month_abbr' => 1,
     'start_year'      => '1999',
-    'end_year'        => '2013', #haha, well...
+    'end_year'        => '2014',
      @_,
   );
 </%init>
diff --git a/httemplate/elements/tr-select-inventory_item.html b/httemplate/elements/tr-select-inventory_item.html
new file mode 100644 (file)
index 0000000..669e85f
--- /dev/null
@@ -0,0 +1,48 @@
+% if ( scalar(@classnums) == 0 ) {
+<& tr-fixed.html, %opt &>
+% } elsif ( scalar(@classnums) == 1 ) {
+%   $opt{'extra_sql'} .= ' AND '.$classnum_sql;
+<& tr-select-table.html,
+  'table'     => 'inventory_item',
+  'name_col'  => 'item',
+  'value_col' => 'item',
+  %opt
+&>
+% } else {
+<& tr-td-label.html, %opt &>
+<TD>
+<& select-tiered.html,
+  'prefix' => $opt{'field'}.'_',
+  'tiers' => [
+    {
+      field         => $opt{'field'}.'_classnum',
+      table         => 'inventory_class',
+      extra_sql     => "WHERE $classnum_sql",
+      name_col      => 'classname',
+      empty_label   => '(all)',
+    },
+    {
+      field         => $opt{'field'},
+      table         => 'inventory_item',
+      name_col      => 'item',
+      value_col     => 'item',
+      link_col      => 'classnum',
+      extra_sql     => delete($opt{'extra_sql'}),
+      disable_empty => 1,
+    },
+  ],
+  %opt,
+&>
+</TD>
+</TR>
+% }
+<%init>
+my %opt = @_;
+my @classnums;
+if (ref($opt{'classnum'})) {
+  @classnums = @{ $opt{'classnum'} };
+} else {
+  @classnums = split(',', $opt{'classnum'});
+}
+my $classnum_sql = 'classnum IN('.join(',', @classnums).')';
+</%init>
index af51487..959ac8d 100644 (file)
@@ -5,7 +5,7 @@
 % } else { 
 
   <TR>
-    <TD ALIGN="right"><% $opt{'label'} || 'Package definition' %></TD>
+    <TD ALIGN="right"><% $opt{'label'} || 'Service definition' %></TD>
     <TD>
       <% include( '/elements/select-part_svc.html',
                     'multiple' => 1,
@@ -21,6 +21,9 @@
 
 my( %opt ) = @_;
 
-$opt{'part_svc'} ||= [ qsearch( 'part_svc', {} ) ]; # { disabled=>'' } )
+my %hash = ();
+$hash{'svcdb'} = $opt{'svcdb'} if $opt{'svcdb'};
+
+$opt{'part_svc'} ||= [ qsearch( 'part_svc', \%hash ) ]; # { disabled=>'' } )
 
 </%init>
index c1df10b..9a670a2 100755 (executable)
@@ -154,15 +154,12 @@ my $controlledbutton = $opt{'control_button'};
 
 my $id = $opt{'id'} || $func_suffix;
 
-my( $add_access_right, $access_right ); 
+my $add_access_right;
 if ($class eq 'C') {
-  $access_right = 'Cancel customer';
   $add_access_right = 'Add on-the-fly cancel reason';
 } elsif ($class eq 'S') {
-  $access_right = 'Suspend customer package';
   $add_access_right = 'Add on-the-fly suspend reason';
 } elsif ($class eq 'R') {
-  $access_right = 'Post credit';
   $add_access_right = 'Add on-the-fly credit reason';
 } else {
   die "illegal class: $class";
index dcc1487..afd3e1f 100644 (file)
@@ -18,7 +18,8 @@ my @options = (
   '' => '',
    1 => 'VoIP without Broadband',
    2 => 'VoIP with Broadband',
-   3 => 'Wholesale VoIP'
+   3 => 'Wholesale VoIP',
+   4 => 'Local Exchange (non-VoIP)',
 );
 
 </%init>
index 9d32a3b..4b31deb 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson(\@areacodes) %>
+<% encode_json(\@areacodes) %>\
 <%init>
 
 my( $state, $svcpart ) = $cgi->param('arg');
index 887b924..0b2f1f1 100644 (file)
@@ -23,15 +23,21 @@ function add_row_callback(rownum, prefix) {
 
 function custnum_update_callback(rownum, prefix) {
   var custnum = document.getElementById('custnum'+rownum).value;
-  document.getElementById('enable_app'+rownum).disabled = (
-    custnum == 0 || 
-    num_open_invoices[rownum] < 2
-  );
+  // if there is a custnum and more than one open invoice, enable
+  // (and check) the box
+  var show_applications = (custnum > 0 && num_open_invoices[rownum] > 1);
+  var enable_app_checkbox = document.getElementById('enable_app'+rownum);
+  enable_app_checkbox.disabled = show_applications;
+
 % if ( $use_discounts ) {
   select_discount_term(rownum, prefix);
 % }
 }
 
+function invnum_update_callback(rownum, prefix) {
+  custnum_update_callback(rownum, prefix);
+}
+
 function select_discount_term(row, prefix) {
   var custnum_obj = document.getElementById('custnum'+prefix+row);
   var select_obj = document.getElementById('discount_term'+prefix+row);
@@ -89,6 +95,17 @@ function toggle_application_row(ev, next) {
         next.call(this, rownum);
       }
     );
+  } else {
+    var row = document.getElementById('row'+rownum);
+    var table_rows = row.parentNode.rows;
+    for (i = row.sectionRowIndex; i < table_rows.count; i++) {
+      if ( table_rows[i].id.indexof('row'+rownum+'.') > -1 ) {
+        table_rows.removeChild(table_rows[i]);
+      } else {
+        break;
+      }
+    }
+    lock_payment_row(rownum, false);
   }
 }
 
@@ -198,7 +215,6 @@ function change_app_amount() {
        && amount_unapplied(rownum) > 0 ) {
 
     create_application_row(rownum, parseInt(appnum) + 1);
-
   }
 }
 
@@ -352,6 +368,7 @@ function preload() {
     footer_align => \@footer_align,
     onchange => \@onchange,
     custnum_update_callback => 'custnum_update_callback',
+    invnum_update_callback => 'invnum_update_callback',
     add_row_callback => 'add_row_callback',
 &>
 
index 7b08f7b..03e336c 100755 (executable)
@@ -32,9 +32,6 @@
 
 <& /elements/standardize_locations.html,
             'form'       => "OrderPkgForm",
-            'onlyship'   => 1,
-            'no_company' => 1,
-            'no_census'  => 1,
             'callback'   => 'document.OrderPkgForm.submit();',
 &>
 
diff --git a/httemplate/misc/change_pkg_contact.html b/httemplate/misc/change_pkg_contact.html
new file mode 100755 (executable)
index 0000000..d9da5be
--- /dev/null
@@ -0,0 +1,70 @@
+<& /elements/header-popup.html, mt("Change Package Contact") &>
+
+<& /elements/error.html &>
+
+<FORM ACTION="<% $p %>misc/process/change_pkg_contact.html" METHOD=POST>
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
+
+<% ntable('#cccccc') %>
+
+  <TR>
+    <TH ALIGN="right"><% mt('Package') |h %></TH>
+    <TD COLSPAN=7>
+      <% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><B><% $part_pkg->pkg |h %></B> - <% $part_pkg->comment |h %>
+    </TD>
+  </TR>
+
+% if ( $cust_pkg->contactnum ) {
+    <TR>
+      <TH ALIGN="right"><% mt('Current Contact') %></TH>
+      <TD COLSPAN=7>
+        <% $cust_pkg->contact_obj->line |h %>
+      </TD>
+    </TR>
+% }
+
+<& /elements/tr-select-contact.html,
+             'label'         => mt('New Contact'), #XXX test
+             'cgi'           => $cgi,
+             'cust_main'     => $cust_pkg->cust_main,
+&>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE    = "submit"
+       VALUE   = "<% $cust_pkg->contactnum ? mt("Change contact") : mt("Add contact") |h %>"
+>
+
+</FORM>
+</BODY>
+</HTML>
+
+<%init>
+
+my $conf = new FS::Conf;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('Change customer package');
+
+my $pkgnum = scalar($cgi->param('pkgnum'));
+$pkgnum =~ /^(\d+)$/ or die "illegal pkgnum $pkgnum";
+$pkgnum = $1;
+
+my $cust_pkg =
+  qsearchs({
+    'table'     => 'cust_pkg',
+    'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+    'hashref'   => { 'pkgnum' => $pkgnum },
+    'extra_sql' => ' AND '. $curuser->agentnums_sql,
+  }) or die "unknown pkgnum $pkgnum";
+
+my $cust_main = $cust_pkg->cust_main
+  or die "can't get cust_main record for custnum ". $cust_pkg->custnum.
+         " ( pkgnum ". cust_pkg->pkgnum. ")";
+
+my $part_pkg = $cust_pkg->part_pkg;
+
+</%init>
index dce04c7..23099c4 100644 (file)
@@ -1,6 +1,5 @@
 <FORM NAME="choosegeocodeform">
 <CENTER><BR><B>Choose tax location</B><BR><BR>
-<P>the geocode is:<% $header %></P>
 <P STYLE="<% $style %>"><% $header %></P>
 
 <SELECT NAME='geocodes' ID='geocodes' STYLE="<% $style %>">
@@ -12,7 +11,7 @@
 %   map { $value{$_} = $location{$_} } qw ( city state )
 %     if $location{country} eq 'CA';
 %
-%   my $value = encode_entities(objToJson({ %value })
+%   my $value = encode_entities(encode_json({ %value })
 %                              );
 %   my $content = '';
 %   $content .= $location->$_. '&nbsp;' x ( $max{$_} - length($location->$_) )
index 57201ea..420e8ea 100644 (file)
@@ -11,16 +11,14 @@ Confirm address standardization
 
 </B><BR><BR>
 <TABLE WIDTH="100%">
-% my @prefixes;
-% if ( $old{onlyship} ) {
-%   @prefixes = ('ship_');
-% } elsif ( $old{same} ) {
+% my @prefixes = ('');
+% if ( $old{same} ) {
 %   @prefixes = ('bill_');
-% } else {
+% } elsif ( $old{billship} ) {
 %   @prefixes = ('bill_', 'ship_');
 % }
 % for my $pre (@prefixes) {
-%   my $name = $pre eq 'ship_' ? 'service' : 'billing';
+%   my $name = $pre eq 'bill_' ? 'billing' : 'service';
 %   if ( $new{$pre.'addr_clean'} ) {
   <TR>
     <TH>Entered <%$name%> address</TH>
@@ -128,6 +126,6 @@ my $q = decode_json($cgi->param('q'));
 my %old = %{ $q->{old} };
 my %new = %{ $q->{new} };
 
-my $addresses = $old{onlyship} ? 'address' : 'addresses';
+my $addresses = $old{billship} ? 'addresses' : 'address';
 
 </%init>
diff --git a/httemplate/misc/confirm-cust_pkg-edit_dates.html b/httemplate/misc/confirm-cust_pkg-edit_dates.html
new file mode 100755 (executable)
index 0000000..8e54852
--- /dev/null
@@ -0,0 +1,289 @@
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('Edit customer package dates');
+
+my %arg = $cgi->Vars;
+
+my $pkgnum = $arg{'pkgnum'};
+$pkgnum =~ /^\d+$/ or die "bad pkgnum '$pkgnum'";
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+my %hash = $cust_pkg->hash;
+foreach (qw( start_date setup bill last_bill contract_end )) {
+  # adjourn, expire, resume not editable this way
+  if( $arg{$_} =~ /^\d+$/ ) {
+    $hash{$_} = $arg{$_};
+  } elsif ( $arg{$_} ) {
+    $hash{$_} = parse_datetime($arg{$_});
+  } else {
+    $hash{$_} = '';
+  }
+}
+
+my (@changes, @confirm, @errors);
+
+my $part_pkg = $cust_pkg->part_pkg;
+my @supp_pkgs = $cust_pkg->supplemental_pkgs;
+my $main_pkg = $cust_pkg->main_pkg;
+
+my $conf = FS::Conf->new;
+my $date_format = $conf->config('date_format') || '%b %o, %Y';
+# Start date
+if ( $hash{'start_date'} != $cust_pkg->get('start_date') and !$hash{'setup'} ) {
+  my $start = '';
+  $start = time2str($date_format, $hash{'start_date'}) if $hash{'start_date'};
+  my $text = 'Set this package';
+  if ( @supp_pkgs ) {
+    $text .= ' and all its supplemental packages';
+  }
+  $text .= ' to start billing';
+  if ( $start ) {
+    $text .= ' on [_1].';
+    push @changes, mt($text, $start);
+  } else {
+    $text .= ' immediately.';
+    push @changes, mt($text);
+  }
+  push @confirm, '';
+}
+
+# Setup date changes
+if ( $hash{'setup'} != $cust_pkg->get('setup') ) {
+  my $setup = time2str($date_format, $hash{'setup'});
+  my $has_setup_fee = grep { $_->part_pkg->option('setup_fee',1) > 0 }
+                      $cust_pkg, @supp_pkgs;
+  if ( !$hash{'setup'} ) {
+    my $text = 'Remove the setup date';
+    $text .= ' from this and all its supplemental packages' if @supp_pkgs;
+    $text .= '.';
+    push @changes, mt($text);
+    if ( $has_setup_fee ) {
+      push @confirm, mt('This will re-charge the customer for the setup fee.');
+    } else {
+      push @confirm, '';
+    }
+  } elsif ( $hash{'setup'} and !$cust_pkg->get('setup') ) {
+    my $text = 'Add a setup date of [_1]';
+    $text .= ' to this and all its supplemental packages' if @supp_pkgs;
+    $text .= '.';
+    push @changes, mt($text, $setup);
+    if ( $has_setup_fee ) {
+      push @confirm, mt('This will prevent charging the setup fee.');
+    } else {
+      push @confirm, '';
+    }
+  } else {
+    my $text = 'Set the setup date to [_1]';
+    $text .= ' on this and all its supplemental packages' if @supp_pkgs;
+    $text .= '.';
+    push @changes, mt($text, $setup);
+    push @confirm, '';
+  }
+}
+
+# Check for start date + setup date
+if ( $hash{'start_date'} and $hash{'setup'} ) {
+  if ( $cust_pkg->get('setup') ) {
+    push @errors, mt('Since the package has already started billing, it '.
+                     'cannot have a start date.');
+  } else {
+    push @errors, mt('You cannot set both a start date and a setup date on '.
+                     'the same package.');
+  }
+}
+
+# Last bill date change
+if ( $hash{'last_bill'} != $cust_pkg->get('last_bill') ) {
+  my $last_bill = time2str($date_format, $hash{'last_bill'});
+  my $name = 'last bill date';
+  $name = 'last renewal date' if $part_pkg->is_prepaid;
+  if ( $hash{'last_bill'} ) {
+    push @changes, mt('Set the [_1] to [_2].', $name, $last_bill);
+  } else {
+    push @changes, mt('Remove the [_1].', $name);
+  }
+  push @confirm, '';
+  # I don't think we want to adjust this on supplemental packages.
+}
+
+# Bill date change
+if ( $hash{'bill'} != $cust_pkg->get('bill') ) {
+  my $bill = time2str($date_format, $hash{'bill'});
+  $bill = 'today' if !$hash{'bill'}; # or 'the end of today'?...
+  my $name = 'next bill date';
+  $name = 'end of the prepaid period' if $part_pkg->is_prepaid;
+  push @changes, mt('Set the [_1] to [_2].', $name, $bill);
+
+  if ( $hash{'bill'} < time and $hash{'bill'} ) {
+    push @confirm, 
+      mt('The customer will be charged for the interval from [_1] until now.',
+         $bill);
+  } elsif ( !$hash{'bill'} and ($hash{'last_bill'} or $hash{'setup'}) ) {
+    my $last_bill = 
+      time2str($date_format, $hash{'last_bill'} || $hash{'setup'});
+    push @confirm,
+      mt('The customer will be charged for the interval from [_1] until now.',
+        $last_bill);
+  } else {
+    push @confirm, '';
+  }
+
+  if ( @supp_pkgs ) {
+    push @changes, '';
+    if ( $cust_pkg->get('bill') and $hash{'bill'} ) {
+      # the package already has a bill date, so adjust the dates 
+      # of supplementals by the same interval
+      my $diff = $hash{'bill'} - $cust_pkg->get('bill');
+      my $sign = $diff < 0 ? -1 : 1;
+      $diff = $diff * $sign / 86400;
+      if ( $diff < 1 ) {
+        $diff = mt('[quant,_1,hour]', int($diff * 24));
+      } else {
+        $diff = mt('[quant,_1,day]', int($diff));
+      }
+      push @confirm,
+        mt('[_1] supplemental package will also be billed [_2] [_3].',
+            (@supp_pkgs > 1 ? 'Each' : 'The'),
+            $diff,
+            ($sign > 0 ? 'later' : 'earlier')
+        );
+    } else {
+      # the package hasn't been billed yet, or you've set bill = null
+      push @confirm,
+        mt('[_1] supplemental package will also be billed on [_2].',
+            (@supp_pkgs > 1 ? 'Each' : 'The'),
+            $bill
+        );
+    }
+  } #if @supp_pkgs
+
+  if ( $main_pkg ) {
+    push @changes, '';
+    push @confirm,
+      mt('This package is a supplemental package.  The bill date of its '.
+         'main package will not be adjusted.');
+  }
+}
+
+# Contract end change
+if ( $hash{'contract_end'} != $cust_pkg->get('contract_end') ) {
+  if ( $hash{'contract_end'} ) {
+    my $contract_end = time2str($date_format, $hash{'contract_end'});
+    push @changes,
+      mt('Set this package\'s contract end date to [_1]', $contract_end);
+  } else {
+    push @changes, mt('Remove this package\'s contract end date.');
+  }
+  if ( @supp_pkgs ) {
+    my $text = 'This change will also apply to ' .
+      (@supp_pkgs > 1 ?
+        'all supplemental packages.':
+        'the supplemental package.');
+    push @confirm, mt($text);
+  } else {
+    push @confirm, '';
+  }
+}
+
+my $title = '';
+if ( @errors ) {
+  $title = 'Error changing package dates';
+} else {
+  $title = 'Confirm date changes';
+}
+</%init>
+<& /elements/header-popup.html, { title => $title, etc => 'BGCOLOR=""' } &>
+<STYLE TYPE="text/css">
+.error { 
+  color: #ff0000;
+  font-weight: bold;
+  text-align: center;
+}
+.confirm { color: #ff0000 }
+.button-container {
+  position: fixed;
+  bottom: 5px;
+  text-align: center;
+  width: 100%
+}
+</STYLE>
+<DIV STYLE="text-align: center; padding:1em">
+<% emt('Package #') %><B><% $pkgnum %></B>: <B><% $cust_pkg->part_pkg->pkg %></B><BR>
+% if ( @changes ) {
+  <% emt('The following changes will be made:') %>
+% } else {
+  <% emt('No changes will be made.') %>
+% }
+</DIV>
+<TABLE WIDTH="100%">
+% if ( @errors ) {
+%   foreach my $error ( @errors ) {
+<TR>
+  <TD><IMG SRC="<%$p%>images/cross.png"></TD>
+  <TD CLASS="error"><% $error %></TD>
+</TR>
+%   }
+% } else {
+%   while (@changes, @confirm) {
+%     my $text = shift @changes;
+%     if (length $text) {
+<TR>
+  <TD><IMG SRC="<%$p%>images/tick.png"></TD>
+  <TD><% $text %></TD>
+</TR>
+%     }
+%     $text = shift @confirm;
+%     if (length $text) {
+<TR>
+  <TD>
+    <INPUT TYPE="checkbox" NAME="areyousure" VALUE=1 onclick="submit_ready()">
+  </TD>
+  <TD CLASS="confirm"><% $text %></TD>
+</TR>
+%     }
+%   }
+% }
+</TABLE>
+%# action buttons
+<DIV CLASS="button-container">
+  <BUTTON TYPE="button" STYLE="width:145px" ID="submit_cancel"\
+    onclick="submit_cancel()">
+    <IMG SRC="<%$p%>images/cross.png" ALT=""> Cancel
+  </BUTTON>
+% if (!@errors ) {
+  <BUTTON TYPE="button" STYLE="width:145px" ID="submit_continue"\
+    onclick="submit_continue()">
+    <IMG SRC="<%$p%>images/tick.png" ALT=""> Continue
+  </BUTTON>
+</DIV>
+% }
+<FORM NAME="DateEditForm" STYLE="display:none" TARGET="_parent" ACTION="<%$p%>edit/process/REAL_cust_pkg.cgi" METHOD="POST">
+% foreach (keys %hash) {
+<INPUT TYPE="hidden" NAME="<%$_%>" VALUE="<% $hash{$_} |h%>">
+% }
+</FORM>
+<SCRIPT>
+function submit_ready() {
+  var ready = true;
+  var checkboxes = document.getElementsByName('areyousure');
+  var i;
+  for (i=0; i < checkboxes.length; i++) {
+    if (! checkboxes[i].checked ) {
+      ready = false;
+    }
+  }
+  document.getElementById('submit_continue').disabled = !ready;
+  return ready;
+}
+function submit_cancel() {
+  parent.nd(1);
+}
+function submit_continue() {
+  if ( submit_ready() ) {
+    document.forms.DateEditForm.submit();
+  }
+}
+submit_ready();
+</SCRIPT>
+<& /elements/footer.html &>
index a277ba4..43b9229 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson( \@return ) %>
+<% encode_json( \@return ) %>\
 <%init>
 
 my( $custnum, $prospectnum, $classnum ) = $cgi->param('arg');
index d26e402..fcd79d7 100644 (file)
@@ -120,9 +120,11 @@ Template:
 
     <TR>
       <TD ALIGN="right" VALIGN="top" STYLE="padding-top:3px">Message: </TD>
-      <TD><& '/elements/htmlarea.html', 
-              'field' => 'html_body',
-              'width' => 600 &></TD>
+      <TD><& /elements/htmlarea.html, 
+               'field' => 'html_body',
+               'width' => 763,
+          &>
+      </TD>
     </TR>
 
   </TABLE>
index 8a67f7b..0de4ace 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson(\@exchanges) %>
+<% encode_json(\@exchanges) %>\
 <%init>
 
 my( $areacode, $svcpart ) = $cgi->param('arg');
index 188c5c3..fab61dd 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson(\%hash) %>
+<% encode_json(\%hash) %>\
 <%init>
 
 my $locationnum = $cgi->param('arg');
@@ -24,8 +24,9 @@ my $cust_location = qsearchs({
 
 my %hash = ();
 %hash = map { $_ => $cust_location->$_() }
-            qw( address1 address2 city county state zip country
-                location_kind location_type location_number )
+            ( FS::cust_main->location_fields,
+              qw( location_kind location_type location_number )
+            )
   if $cust_location;
 
 </%init>
index b07da97..cec0e31 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson(\@macs) %>
+<% encode_json(\@macs) %>\
 <%init>
 
 # XXX: this should be agent-virtualized / limited
@@ -13,13 +13,8 @@ die "unknown devicepart $devicepart" unless $part_device;
 my $inventory_class = $part_device->inventory_class;
 die "devicepart $devicepart has no inventory" unless $inventory_class;
 
-my @inventory_item =
+my @macs =
+  map $_->item,
     qsearch('inventory_item', { 'classnum' => $inventory_class->classnum } );
 
-my @macs;
-
-foreach my $inventory_item ( @inventory_item ) {
-    push @macs, $inventory_item->item;
-}
-
 </%init>
index 8acae2b..a872d49 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson( $return ) %>
+<% encode_json( $return ) %>\
 <%init>
 
 my $return;
diff --git a/httemplate/misc/manage_cust_email.html b/httemplate/misc/manage_cust_email.html
new file mode 100644 (file)
index 0000000..3ece459
--- /dev/null
@@ -0,0 +1,106 @@
+<& /elements/header.html, 'Manage customer email settings' &>
+<STYLE TYPE="text/css">
+.hidden { display: none }
+</STYLE>
+<& /elements/xmlhttp.html,
+    url => $p.'misc/xmlhttp-cust_main-email_search.html',
+    subs => ['email_search']
+&>
+<SCRIPT TYPE="text/javascript">
+
+function receive_search(result) {
+  var recs = JSON.parse(result);
+  var tbody = document.getElementById('tbody_results');
+  var j = tbody.rows.length;
+  for(var i = 0; i < j; i++) {
+    tbody.deleteRow(tbody.rows[i]);
+  }
+  if (recs.length > 0) {
+    for(var i = 0; i < recs.length; i++) {
+      var rec = recs[i];
+      var row = tbody.insertRow(i);
+      row.style.backgroundColor = (i % 2 ? '#eeeeee' : '#ffffff');
+
+      var cell = row.insertCell(0); // custnum
+      cell.appendChild( document.createTextNode(rec[0]) );
+      cell = row.insertCell(1);     // customer name
+      cell.appendChild( document.createTextNode(rec[1]) );
+      cell = row.insertCell(2);     // email
+      cell.appendChild( document.createTextNode(rec[2]) );
+
+      cell = row.insertCell(3);     // invoice_email
+      var input = document.createElement('INPUT');
+      input.type = 'hidden';
+      input.name = 'custnum';
+      input.value = rec[0];
+      cell.appendChild(input);
+
+      input = document.createElement('INPUT');
+      input.type = 'checkbox';
+      input.name = 'custnum' + rec[0] + '_invoice_email';
+      input.value = 'Y';
+      input.checked = (rec[3] != 'Y');
+      cell.appendChild(input);
+      cell.style.textAlign = 'center';
+      
+      cell = row.insertCell(4);     // message_email
+      input = document.createElement('INPUT');
+      input.type = 'checkbox';
+      input.name = 'custnum' + rec[0] + '_message_email';
+      input.value = 'Y';
+      input.checked = (rec[4] != 'Y');
+      cell.appendChild(input);
+      cell.style.textAlign = 'center';
+    }
+    document.getElementById('div_found').style.display = '';
+  } else {
+    document.getElementById('div_notfound').style.display = '';
+  }
+}
+
+function start_search() {
+  document.getElementById('div_found').style.display = 'none';
+  document.getElementById('div_notfound').style.display = 'none';
+  var email = document.getElementById('input_email').value;
+  email_search(email, receive_search);
+}
+% if ( $cgi->param('search') ) {
+window.onload = start_search;
+% }
+</SCRIPT>
+<FORM ACTION="<%$p%>misc/process/manage_cust_email.html" METHOD="POST">
+<DIV>
+% if ( $cgi->param('done') ) {
+<P STYLE="font-weight: bold; color: #00ff00">Changes saved.</P>
+% } elsif ( $cgi->param('error') ) {
+<P STYLE="font-weight: bold; color: #ff0000"><% $cgi->param('error') |h %></P>
+% }
+  Email address: 
+  <INPUT TYPE="text" ID="input_email" NAME="search"\
+         VALUE="<% $cgi->param('search') |h %>">
+  <INPUT TYPE="button" onclick="start_search()" VALUE="find">
+</DIV>
+<DIV ID="div_notfound" STYLE="display: none; padding: 1em">
+No matching email addresses found.
+</DIV>
+<DIV ID="div_found" STYLE="display: none">
+<TABLE CLASS="grid" STYLE="border-spacing: 0px">
+  <THEAD>
+    <TR STYLE="background-color: #dddddd">
+      <TH>#</TH>
+      <TH>Customer</TH>
+      <TH>Email</TH>
+      <TH>Send invoices</TH>
+      <TH>Send other notices</TH>
+    </TR>
+  </THEAD>
+  <TBODY ID="tbody_results"></TBODY>
+</TABLE>
+<INPUT TYPE="submit" VALUE="Save changes">
+</FORM>
+<& /elements/footer.html &>
+<%init>
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Edit customer');
+
+</%init>
index bfc7b69..e09ba98 100644 (file)
   &>
 % }
 
+<& /elements/tr-select-contact.html,
+             'cgi'           => $cgi,
+             'cust_main'     => $cust_main,
+             'prospect_main' => $prospect_main,
+&>
+
 % if ( $cgi->param('lock_locationnum') ) {
 
     <INPUT TYPE  = "hidden"
 
   <& /elements/standardize_locations.html,
                 'form'       => "OrderPkgForm",
-                'onlyship'   => 1,
-                'no_company' => 1,
-                'no_census'  => 1,
                 'callback'   => 'document.OrderPkgForm.submit();',
   &>
 
diff --git a/httemplate/misc/part_export/huawei_hlr-import_sim.html b/httemplate/misc/part_export/huawei_hlr-import_sim.html
new file mode 100644 (file)
index 0000000..9b87b3d
--- /dev/null
@@ -0,0 +1,52 @@
+<& /elements/header-popup.html, 'Import SIMs' &>
+Import a file containing SIM card properties.<BR>
+Each row should contain the following fields, separated by spaces:<BR>
+IMSI, ICCID, PIN1, PUK1, PIN2, PUK2, ACC, Ki<BR>
+<BR>
+<& /elements/form-file_upload.html,
+     'name'      => 'ImportForm',
+     'action'    => 'process/huawei_hlr-import_sim.html',
+     'num_files' => 1,
+     'fields'    => [ 'exportnum', 'classnum', 'agentnum', ],
+     'message'   => 'Inventory import successful',
+     'onsubmit'  => "document.ImportForm.submitButton.disabled=true;",
+&>
+<TABLE CLASS="inv" WIDTH="100%">
+  <INPUT TYPE="hidden" NAME="exportnum" VALUE="<%$exportnum%>">
+  <& /elements/file-upload.html,
+    'field' => 'file',
+    'label' => 'Filename',
+  &>
+  <& /elements/tr-select-agent.html,
+    'disable_empty' => 1,
+  &>
+  <& /elements/tr-select-table.html,
+    'table'     => 'inventory_class',
+    'name_col'  => 'classname',
+    'label'     => 'Inventory class',
+    'disable_empty' => 1,
+  &>
+
+  <TR>
+    <TD COLSPAN=2 ALIGN="center" STYLE="padding-top:6px">
+      <INPUT TYPE  = "submit"
+             NAME  = "submitButton"
+             ID    = "submitButton"
+             VALUE = "Import file"
+      >
+    </TD>
+  </TR>
+
+</TABLE>
+
+</FORM>
+
+<%init>
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my ($exportnum) = $cgi->keywords;
+$exportnum =~ /^\d+$/ or die "bad exportnum '$exportnum'";
+my $part_export = FS::part_export->by_key($exportnum)
+  or die "export $exportnum not found";
+</%init>
diff --git a/httemplate/misc/part_export/process/huawei_hlr-import_sim.html b/httemplate/misc/part_export/process/huawei_hlr-import_sim.html
new file mode 100644 (file)
index 0000000..d46700d
--- /dev/null
@@ -0,0 +1,10 @@
+<% $server->process %>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $server = new FS::UI::Web::JSRPC
+  'FS::part_export::huawei_hlr::process_import_sim', $cgi;
+
+</%init>
index 0602561..a86164d 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson(\@output) %>
+<% encode_json(\@output) %>\
 <%init>
 
 my $conf = new FS::Conf;
index 5084628..a048280 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson(\@phonenums) %>
+<% encode_json(\@phonenums) %>\
 <%init>
 
 my( $exchangestring, $svcpart ) = $cgi->param('arg');
diff --git a/httemplate/misc/process/change-password.html b/httemplate/misc/process/change-password.html
new file mode 100644 (file)
index 0000000..7cab9c4
--- /dev/null
@@ -0,0 +1,26 @@
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+$cgi->param('svcnum') =~ /^(\d+)$/ or die "illegal svcnum";
+my $svcnum = $1;
+my $svc_acct = FS::svc_acct->by_key($svcnum)
+  or die "svc_acct $svcnum not found";
+my $part_svc = $svc_acct->part_svc;
+die "access denied" unless (
+  $curuser->access_right('Provision customer service') or
+  ( $curuser->access_right('Edit password') and 
+    ! $part_svc->restrict_edit_password )
+  );
+my $error = $svc_acct->set_password($cgi->param('password'))
+        ||  $svc_acct->replace;
+
+# annoyingly specific to view/svc_acct.cgi, for now...
+$cgi->delete('password');
+</%init>
+% if ( $error ) {
+%   $cgi->param('svcnum', $svcnum);
+%   $cgi->param("changepw${svcnum}_error", $error);
+% } else {
+%   $cgi->query_string($svcnum);
+% }
+<% $cgi->redirect($fsurl.'view/svc_acct.cgi?'.$cgi->query_string) %>
diff --git a/httemplate/misc/process/change_pkg_contact.html b/httemplate/misc/process/change_pkg_contact.html
new file mode 100644 (file)
index 0000000..2795c11
--- /dev/null
@@ -0,0 +1,49 @@
+<% header(emt("Package contact $past_method")) %>
+  <SCRIPT TYPE="text/javascript">
+    window.top.location.reload();
+  </SCRIPT>
+  </BODY>
+</HTML>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Change customer package');
+
+#untaint pkgnum
+my $pkgnum = $cgi->param('pkgnum');
+$pkgnum =~ /^(\d+)$/ or die "Illegal pkgnum";
+$pkgnum = $1;
+
+my $cust_pkg = qsearchs( 'cust_pkg', {'pkgnum'=>$pkgnum} ); #needs agent virt
+
+my $contactnum = $cgi->param('contactnum');
+$contactnum =~ /^(-?\d*)$/ or die "Illegal contactnum";
+$contactnum = $1;
+
+my $past_method = $cust_pkg->contactnum ? 'changed' : 'added';
+
+my $error = '';
+
+if ( $contactnum == -1 ) {
+
+  #little false laziness w/edit/process/quick-cust_pkg.cgi, also the whole
+  # thing should be a single transaction
+  my $contact = new FS::contact {
+    'custnum' => $cust_pkg->custnum,
+    map { $_ => scalar($cgi->param("contactnum_$_")) } qw( first last )
+  };
+  $error = $contact->insert;
+  $cust_pkg->contactnum( $contact->contactnum );
+
+} else {
+  $cust_pkg->contactnum($contactnum);
+}
+
+$error ||= $cust_pkg->replace;
+
+if ($error) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "change_pkg_contact.html?". $cgi->query_string );
+}
+
+</%init>
diff --git a/httemplate/misc/process/manage_cust_email.html b/httemplate/misc/process/manage_cust_email.html
new file mode 100644 (file)
index 0000000..5bf1470
--- /dev/null
@@ -0,0 +1,32 @@
+<% $cgi->redirect($fsurl.'misc/manage_cust_email.html?' .
+                  $cgi->query_string) %>
+<%init>
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Edit customer');
+
+my $error;
+foreach my $custnum ($cgi->param('custnum')) {
+  my $cust = FS::cust_main->by_key($custnum)
+    or die "customer not found: $custnum\n";
+  my $new_invoice_noemail = 
+    $cgi->param('custnum'.$custnum.'_invoice_email') ? '' : 'Y';
+  my $new_message_noemail =
+    $cgi->param('custnum'.$custnum.'_message_email') ? '' : 'Y';
+  if ( $new_invoice_noemail ne $cust->invoice_noemail
+    or $new_message_noemail ne $cust->message_noemail ) {
+
+    $cust->set('invoice_noemail', $new_invoice_noemail);
+    $cust->set('message_noemail', $new_message_noemail);
+    $error ||= $cust->replace;
+
+  }
+  $cgi->delete('custnum'.$custnum.'_invoice_email');
+  $cgi->delete('custnum'.$custnum.'_message_email');
+}
+$cgi->delete('custnum');
+if ( $error ) {
+  $cgi->param('error' => $error); # probably unnecessary...
+} else {
+  $cgi->param('done' => 1) unless $error;
+}
+</%init>
index 506e266..981614e 100644 (file)
@@ -210,7 +210,15 @@ if ( $cgi->param('save') ) {
     $new->set( 'paycvv' => '');
   }
 
-  $new->set( $_ => $cgi->param($_) ) foreach @{$payby2fields{$payby}};
+  if ( $payby eq 'CARD' ) {
+    my $bill_location = FS::cust_location->new;
+    $bill_location->set( $_ => $cgi->param($_) )
+      foreach @{$payby2fields{$payby}};
+    $new->set('bill_location' => $bill_location);
+    # will do nothing if the fields are all unchanged
+  } else {
+    $new->set( $_ => $cgi->param($_) ) foreach @{$payby2fields{$payby}};
+  }
 
   my $error = $new->replace($cust_main);
   errorpage("payment processed successfully, but error saving info: $error")
index 2450ea3..31538b0 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson(\@regions) %>
+<% encode_json(\@regions) %>\
 <%init>
 
 my( $state, $svcpart ) = $cgi->param('arg');
index 9880571..6182653 100644 (file)
@@ -1,4 +1,4 @@
-<% encode_json($return) %>
+<% encode_json($return) %>\
 <%init>
 
 local $SIG{__DIE__}; #disable Mason error trap
@@ -16,12 +16,10 @@ my %old = %{ decode_json($cgi->param('arg')) }
 
 my %new;
 
-my @prefixes;
-if ($old{onlyship}) {
-  @prefixes = ('ship_');
-} elsif ( $old{same} ) {
+my @prefixes = ('');
+if ( $old{same} ) {
   @prefixes = ('bill_');
-} else {
+} elsif ( $old{billship} ) {
   @prefixes = ('bill_', 'ship_');
 }
 my $all_same = 1;
@@ -44,6 +42,8 @@ foreach my $pre ( @prefixes ) {
     $all_same = 0 if ( $new{$pre.$_} ne $old{$pre.$_} );
     last if !$all_same;
   }
+
+  $all_same = 0 if $new{$pre.'error'};
 }
 
 my $return = { old => \%old, new => \%new, all_same => $all_same };
index d3dc36a..ed7bd01 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson($return) %>
+<% encode_json($return) %>\
 <%init>
 
 my $DEBUG = 0;
index 46f15d1..c60a0b0 100644 (file)
@@ -1,4 +1,4 @@
-<% encode_json(\@return) %>
+<% encode_json(\@return) %>\
 <%init>
 
 my $curuser = $FS::CurrentUser::CurrentUser;
@@ -6,13 +6,15 @@ die 'access denied' unless $curuser->access_right('View invoices');
 my @return;
 if ( $cgi->param('sub') eq 'custnum_search_open' ) { 
   my $custnum = $cgi->param('arg');
-  #warn "searching invoices for $custnum\n";
-  my $cust_main = FS::cust_main->by_key($custnum);
-  @return = map { 
-    +{ $_->hash, 
-      'owed' => $_->owed }
-  } $cust_main->open_cust_bill
-    if $curuser->agentnums_href->{ $cust_main->agentnum };
+  if ( $custnum =~ /^(\d+)$/ ) {
+#warn "searching invoices for $custnum\n";
+    my $cust_main = FS::cust_main->by_key($custnum);
+    @return = map { 
+      +{ $_->hash, 
+        'owed' => $_->owed }
+    } $cust_main->open_cust_bill
+      if $curuser->agentnums_href->{ $cust_main->agentnum };
+  }
 }
 
 </%init>
index 9935046..c0db3e2 100644 (file)
@@ -1,8 +1,8 @@
-<% to_json($return) %>
+<% encode_json($return) %>\
 <%init>
 
 my $curuser = $FS::CurrentUser::CurrentUser;
-die "access denied" unless $curuser->access_right('Post credit');
+die "access denied" unless $curuser->access_right('Credit line items');
 
 my $DEBUG = 0;
 
index 4b00898..4c708a4 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson($return) %>
+<% encode_json($return) %>\
 <%init>
 
 my %arg = $cgi->param('arg');
index b524e69..36b18b4 100644 (file)
@@ -16,7 +16,7 @@
 %     }
 %   }
 %
-<% objToJson($return) %>
+<% encode_json($return) %>\
 % } 
 <%init>
 
diff --git a/httemplate/misc/xmlhttp-cust_main-email_search.html b/httemplate/misc/xmlhttp-cust_main-email_search.html
new file mode 100644 (file)
index 0000000..0d83082
--- /dev/null
@@ -0,0 +1,29 @@
+<% encode_json(\@result) %>\
+<%init>
+die 'access denied'
+  unless $FS::CurrentUser::CurrentUser->access_right('Edit customer');
+
+my $sub = $cgi->param('sub');
+my $email = $cgi->param('arg');
+my @where = (
+  "cust_main_invoice.dest != 'POST'",
+  "cust_main_invoice.dest LIKE ".dbh->quote('%'.$email.'%'),
+  $FS::CurrentUser::CurrentUser->agentnums_sql(table => 'cust_main'),
+);
+my @cust_main = qsearch({
+  'table'     => 'cust_main',
+  'select'    => 'cust_main.*, cust_main_invoice.dest',
+  'addl_from' => 'JOIN cust_main_invoice USING (custnum)',
+  'extra_sql' => 'WHERE '.join(' AND ', @where),
+});
+
+my @result = map {
+  [ $_->custnum,
+    $_->name,
+    $_->dest,
+    $_->invoice_noemail,
+    $_->message_noemail,
+  ]
+} @cust_main;
+
+</%init>
index acf7e70..73c9ff8 100644 (file)
@@ -5,7 +5,7 @@
 %                                  # cust_main-agent_custid-format') eq 'ww?d+'
 %      $return = findbycustnum_or_agent_custid($1);
 %   }
-<% objToJson($return) %>
+<% encode_json($return) %>\
 % } elsif ( $sub eq 'smart_search' ) {
 %
 %   my $string = $cgi->param('arg');
 %                    @cust_main
 %                ];
 %     
-<% objToJson($return) %>
+<% encode_json($return) %>\
 % } elsif ( $sub eq 'invnum_search' ) {
 %
 %   my $string = $cgi->param('arg');
 %   if ( $string =~ /^(\d+)$/ ) {
 %     my $inv = qsearchs('cust_bill', { 'invnum' => $1 });
 %     my $return = $inv ? findbycustnum($inv->custnum) : [];
-<% objToJson($return) %>
+<% encode_json($return) %>\
 %   } else { #return nothing
 []
 %   }
@@ -47,7 +47,7 @@
 %       city => $_->city,
 %     };
 %   }
-<% objToJson($return) %>
+<% encode_json($return) %>\
 % }
 <%init>
 
index e993032..01baa3f 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson($return) %>
+<% encode_json($return) %>\
 <%init>
 
 my $conf = new FS::Conf;
diff --git a/httemplate/misc/xmlhttp-svc_broadband-search.cgi b/httemplate/misc/xmlhttp-svc_broadband-search.cgi
new file mode 100644 (file)
index 0000000..578e614
--- /dev/null
@@ -0,0 +1,22 @@
+% if ( $sub eq 'smart_search' ) {
+%
+%   my $string = $cgi->param('arg');
+%   my @svc_broadband = FS::svc_broadband->smart_search( $string );
+%   my $return = [ map { my $cust_pkg = $_->cust_svc->cust_pkg;
+%                        [ $_->svcnum,
+%                          $_->label. ( $cust_pkg
+%                                        ? ' ('. $cust_pkg->cust_main->name. ')'
+%                                        : ''
+%                                     ),
+%                        ];
+%                      } 
+%                    @svc_broadband,
+%                ];
+%     
+<% encode_json($return) %>\
+% }
+<%init>
+
+my $sub = $cgi->param('sub');
+
+</%init>
index c4fef03..6b94f71 100644 (file)
@@ -49,6 +49,7 @@ unless ( $error ) { # if ($access_user) {
 
   #XXX autogen
   my @paramlist = qw( locale menu_position default_customer_view 
+                      history_order
                       spreadsheet_format mobile_menu
                       enable_fuzzy_on_exact
                       disable_html_editor disable_enter_submit_onetimecharge
@@ -57,7 +58,7 @@ unless ( $error ) { # if ($access_user) {
                       vonage-fromnumber vonage-username vonage-password
                       cust_pkg-display_times
                       show_pkgnum show_confitem_counts export_getsettings
-                      show_db_profile save_db_profile
+                      show_db_profile save_db_profile save_tmp_typesetting
                       height width availHeight availWidth colorDepth
                     );
 
index 1e9671d..5babb01 100644 (file)
@@ -75,6 +75,21 @@ Interface
       </SELECT>
     </TD>
   </TR>
+
+% my $history_order = $curuser->option('history_order') || 'oldest';
+  <TR>
+    <TH ALIGN="right">Customer history sort order: </TH>
+    <TD COLSPAN=2>
+      <& /elements/select.html,
+        field       => 'history_order',
+        curr_value  => $history_order,
+        options     => [ 'oldest', 'newest' ],
+        labels      => { 'oldest' => 'Oldest first',
+                         'newest' => 'Newest first',
+                       },
+      &>
+    </TD>
+  </TR>
   
   <TR>
     <TH ALIGN="right">Spreadsheet download format: </TH>
@@ -92,7 +107,7 @@ Interface
   </TR>
 
  <TR>
-    <TH ALIGN="right" COLSPAN=1>Enable approximate customer searching even when an exact match is found: </TH>
+    <TH ALIGN="right" COLSPAN=1>Enable approximate customer searching <BR>even when an exact match is found: </TH>
     <TD ALIGN="left" COLSPAN=2>
       <INPUT TYPE="checkbox" NAME="enable_fuzzy_on_exact" VALUE="1" <% $curuser->option('enable_fuzzy_on_exact') ? 'CHECKED' : '' %>>
     </TD>
@@ -157,6 +172,10 @@ Development
     <TH>Save database profiling logs (when available): </TH>
     <TD><INPUT TYPE="checkbox" NAME="save_db_profile" VALUE="1" <% $curuser->option('save_db_profile') ? 'CHECKED' : '' %>></TD>
   </TR>
+  <TR>
+    <TH>Save temporary invoice typesetting files: </TH>
+    <TD><INPUT TYPE="checkbox" NAME="save_tmp_typesetting" VALUE="1" <% $curuser->option('save_tmp_typesetting') ? 'CHECKED' : '' %>></TD>
+  </TR>
 
 </TABLE>
 <BR>
index 6f5fcdf..eed3df9 100755 (executable)
@@ -3,6 +3,14 @@
 <Form_477_submission xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://specialreports.fcc.gov/wcb/Form477/XMLSchema-instance/form_477_upload_Schema.xsd" >
 % } else { #html
 <& /elements/header.html, "FCC Form 477 Results - $state" &>
+%# XXX when we stop supporting IE8, add this to freeside.css using :nth-child
+%# selectors, and remove it from everywhere else
+<STYLE TYPE="text/css">
+.grid TH { background-color: #cccccc; padding: 0px 3px 2px; text-align: right }
+.row0 TD { background-color: #eeeeee; padding: 0px 3px 2px; text-align: right }
+.row1 TD { background-color: #ffffff; padding: 0px 3px 2px; text-align: right }
+</STYLE>
+
 <TABLE WIDTH="100%">
   <TR>
     <TD></TD>
 %         if ( $type eq 'xml' ) {
 <<% 'Part_IA_'. chr(65 + $tech) %>>
 %         }
-<& "477part${part}_summary.html", 'tech_code' => $tech, 'url' => $url &>
-<& "477part${part}_detail.html", 'tech_code' => $tech, 'url' => $url &>
+<& "477part${part}.html",
+    'tech_code' => $tech,
+    'url' => $url,
+    'type' => $type
+&>
 %         if ( $type eq 'xml' ) {
 </<% 'Part_IA_'. chr(65 + $tech) %>>
 %         }
@@ -97,6 +108,11 @@ for(my $i=0; $i < scalar(@part2b_row_option); $i++) {
     &FS::Report::FCC_477::save_fcc477map("part2b_row_option_$i",$part2b_row_option[$i]);
 }
 
+my $part5_report_option = $cgi->param('part5_report_option');
+if ( $part5_report_option ) {
+  FS::Report::FCC_477::save_fcc477map('part5_report_option', $part5_report_option);
+}
+
 my $url_mangler = sub {
   my $part = shift;
   my $url = $cgi->url('-path_info' => 1, '-full' => 1);
diff --git a/httemplate/search/477partIA.html b/httemplate/search/477partIA.html
new file mode 100755 (executable)
index 0000000..1cd0b70
--- /dev/null
@@ -0,0 +1,165 @@
+% if ( $opt{'type'} eq 'xml' ) {
+%# container element <Part_IA_$tech> is in 477.html
+%   my $col = 'a';
+%   foreach ( @summary_row ) {
+%     my $el = $xml_prefix . $col . '1'; # PartIA_Aa1, PartIA_Ab1, etc.
+  <<% $el %>><% $_ %><<% "/$el" %>>
+%     $col++;
+%   }
+%   foreach my $col_data ( @data ) { 
+%     my $row = 1;
+%     foreach my $cell ( @$col_data ) {
+%       my $el = $xml_prefix . $col . $row; # PartIA_Af1, PartIA_Af2...
+  <<% $el %>><% $cell->[0] %><<% "/$el" %>>
+%       if ( $percentages ) {
+%         $el = $xml_percent . $col . $row; # Part_p_IA_Af1, ...
+  <<% $el %>><% $cell->[1] %><<% "/$el" %>>
+%       }
+%       $row++;
+%     } # foreach $cell
+%     $col++;
+%   } # foreach $col_data
+% } else { # not XML
+
+<H2><% $title %> totals</H2>
+<& /elements/table-grid.html &>
+  <TR>
+%   foreach ( 'Total Connections',
+%             '% owned loop',
+%             '% billed to end users',
+%             '% residential',
+%             '% residential > 200 kbps') {
+    <TH WIDTH="20%"><% $_ |h %></TH>
+%   }
+  </TR>
+  <TR CLASS="row0">
+%   foreach ( @summary_row ) {
+    <TD><% $_ %></TD>
+%   }
+  </TR>
+</TABLE>
+<H2><% $title %> breakdown by speed</H2>
+<TABLE CLASS="grid" CELLSPACING=0>
+  <TR>
+    <TH WIDTH="12%"></TH>
+%   for (my $col = 0; $col < scalar(@download_option); $col++) {
+    <TH WIDTH="11%">
+      <% $FS::Report::FCC_477::download[$col] |h %>
+    </TH>
+%   }
+  </TR>
+% for (my $row = 0; $row < scalar(@upload_option); $row++) {
+  <TR CLASS="row<% $row % 2%>">
+    <TD STYLE="text-align: left; font-weight: bold">
+%     if ( $asymmetric ) {
+      <% $FS::Report::FCC_477::upload[$row] |h %>
+%     }
+    </TD>
+%   for (my $col = 0; $col < scalar(@download_option); $col++) {
+    <TD>
+      <% $data[$col][$row][0] %>
+%     if ( $percentages ) {
+      <BR><% $data[$col][$row][1] %>
+%     }
+    </TD>
+%   } # for $col
+  </TR>
+% } # for $row
+</TABLE>
+% }
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('List packages');
+
+my %opt = @_;
+my %search_hash;
+  
+for ( qw(agentnum state) ) {
+  $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
+}
+$search_hash{'status'} = 'active';
+$search_hash{'country'} = 'US';
+$search_hash{'classnum'} = [ $cgi->param('classnum') ];
+
+# arrays of report_option_ numbers, running parallel to 
+# the download and upload speed arrays
+my @download_option = $cgi->param('part1_column_option');
+my @upload_option = $cgi->param('part1_row_option');
+
+my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi);
+
+my $total_count = 0;
+my $total_residential = 0;
+my $above_200 = 0;
+my $tech_code = $opt{tech_code};
+my $technology = $FS::Report::FCC_477::technology[$tech_code] || 'unknown';
+my $title = "Part IA $technology";
+my $xml_prefix = 'PartIA_'. chr(65 + $tech_code);
+my $xml_percent = 'Part_p_IA_'. chr(65 + $tech_code); # yes, seriously
+
+# whether to show the results as a matrix (upload speeds in rows) or a single
+# row
+my $asymmetric = 1;
+if ( $technology eq 'Symmetric xDSL' or $technology eq 'Other Wireline' ) {
+  $asymmetric = 0;
+  @upload_option = ( undef );
+}
+# whether to show residential percentages in each cell of the matrix
+my $percentages = ($technology eq 'Terrestrial Mobile Wireless');
+
+my $query = FS::cust_pkg->search(\%search_hash);
+my $count_query = $query->{'count_query'};
+
+my $is_residential = " AND COALESCE(cust_main.company, '') = ''";
+my $has_option = sub {
+  my $optionnum = shift;
+  $optionnum =~ /^\d+$/ ?
+  " AND EXISTS(
+    SELECT 1 FROM part_pkg_option
+    WHERE part_pkg_option.pkgpart = part_pkg.pkgpart
+    AND optionname = 'report_option_$optionnum'
+    AND optionvalue = '1'
+  )" : '';
+};
+
+# limit to those that have technology option $tech_code
+$count_query .= $has_option->($technology_option[$tech_code]);
+
+my @data;
+for ( my $row = 0; $row < scalar @upload_option; $row++ ) {
+  for ( my $col = 0; $col < scalar @download_option; $col++ ) {
+
+    my $this_count_query = $count_query .
+                           $has_option->($upload_option[$row]) .
+                           $has_option->($download_option[$col]);
+
+    my $count = FS::Record->scalar_sql($this_count_query);
+    my $residential = FS::Record->scalar_sql($this_count_query . $is_residential);
+
+    my $percent = sprintf('%.2f', $count ? 100 * $residential / $count : 0);
+    $data[$col][$row] = [ $count, $percent ];
+
+    $total_count += $count;
+    $total_residential += $residential;
+    $above_200 += $residential if $row > 0 or !$asymmetric;
+  }
+}
+
+my $total_percentage =
+  sprintf("%.2f", $total_count ? 100*$total_residential/$total_count : 0);
+
+my $above_200_percentage =
+  sprintf("%.2f", $total_count ? 100*$above_200/$total_count : 0);
+
+my @summary_row = (
+  $total_count,
+  100.00, # own local loop--consistent with previous practice, but probably wrong
+  100.00, # billed to end user--also wrong
+  $total_percentage, # residential percentage
+  $above_200_percentage,
+);
+
+</%init>
diff --git a/httemplate/search/477partIA_detail.html b/httemplate/search/477partIA_detail.html
deleted file mode 100755 (executable)
index 66f3a86..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-<% include( 'elements/search.html',
-                  'html_init'        => $html_init,
-                  'name'             => 'lines',
-                  'query'            => $query,
-                  'count_query'      => $count_query,
-                  'really_disable_download' => 1,
-                  'disable_download' => 1,
-                  'nohtmlheader'     => 1,
-                  'disable_total'    => 1,
-                  'header'           => [ '', @column_option_name ],
-                  'xml_elements'     => [ @xml_elements ],
-                  'xml_omit_empty'   => 1,
-                  'fields'           => [  @fields ],
-              )
-%>
-<%init>
-
-my $curuser = $FS::CurrentUser::CurrentUser;
-
-die "access denied"
-  unless $curuser->access_right('List packages');
-
-my %opt = @_;
-my %search_hash = ();
-  
-for ( qw(agentnum magic state) ) {
-  $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
-}
-$search_hash{'country'} = 'US';
-
-$search_hash{'classnum'} = [ $cgi->param('classnum') ];
-
-my @column_option = grep { /^\d+/ } $cgi->param('part1_column_option')
-  if $cgi->param('part1_column_option');
-
-my @row_option = grep { /^\d+/ } $cgi->param('part1_row_option')
-  if $cgi->param('part1_row_option');
-
-my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi);
-
-my @column_option_name = scalar(@column_option)
-  ? ( map { my $part_pkg_report_option = 
-              qsearchs({ 'table' => 'part_pkg_report_option',
-                         'hashref' => { num => $_ },
-                      });
-            $part_pkg_report_option ? $part_pkg_report_option->name
-                                    : 'no such report option';
-          } @column_option
-    )
-  : ( 'all packages' );
-
-my $where = join(' OR ', map { "num = $_" } @row_option );
-my %row_option_name = $where ?
-                        ( map { $_->num => $_->name }
-                          qsearch({ 'table' => 'part_pkg_report_option',
-                                    'hashref' => {},
-                                    'extra_sql' => "WHERE $where",
-                                 })
-                        ) :
-                        ();
-
-my $tech_code = $opt{tech_code};
-my $technology = $FS::Report::FCC_477::technology[$tech_code] || 'unknown';
-my $html_init = "<H2>Part IA $technology breakdown by speeds</H2>";
-my $xml_prefix = 'PartIA_'. chr(65 + $tech_code);
-
-if ($cgi->param('_type') eq 'xml') {
-  #rotate data pi/2
-  my @temp = @column_option;
-  @column_option = @row_option;
-  @row_option = @temp;
-}
-
-my $query = 'SELECT '. join(' UNION ALL SELECT ',@row_option);
-my $count_query = 'SELECT '. scalar(@row_option);
-
-my $xml_element = 'OOPS, I was never set';
-my $rowchar = 101; # 'e' -- rows are columns! (pi/2)
-
-my $value = sub {
-  my ($rowref, $column) = (shift, shift);
-  my $row = $rowref->[0];
-
-  if ($column eq 'name') {
-    return $row_option_name{$row} || 'no such report option';
-  } elsif ( $column =~ /^(\d+)$/ ) {
-    my @report_option = ( $row || '',
-                          $column_option[$column] || '',
-                          $technology_option[$tech_code] || '',
-                        );
-
-    my ( $count, $residential ) = FS::cust_pkg->fcc_477_count(
-      { %search_hash, 'report_option' => join(',', @report_option) }
-    );
-
-    my $percentage = sprintf('%.2f', $count ? 100 * $residential / $count : 0);
-    my $return = $count;
-
-    if ($cgi->param('_type') eq 'xml') {
-      $rowchar++ if $column == 0;
-      $xml_element = $xml_prefix. chr($rowchar). ($column+1);
-      $return = '' if $count == 0 and $cgi->param('_type') eq 'xml';
-    } else {
-      $return .= "<BR>$percentage% residential";
-    }
-
-    return $return;
-  } else {
-    return '<FONT SIZE="+1" COLOR="#ff0000">Bad call to column_value</FONT>';
-  }
-};
-
-my @fields = map { my $ci = $_; sub { &{$value}(shift, $ci); } }
-            ( 'name', (0 .. $#column_option) );
-shift @fields if $cgi->param('_type') eq 'xml';
-
-my @xml_elements = (  # -- columns are rows! (pi/2)
-  sub { return $xml_element; },
-  sub { return $xml_element; },
-  sub { return $xml_element; },
-  sub { return $xml_element; },
-  sub { return $xml_element; },
-  sub { return $xml_element; },
-  sub { return $xml_element; },
-  sub { return $xml_element; },
-  sub { return $xml_element; },
-);
-
-</%init>
diff --git a/httemplate/search/477partIA_summary.html b/httemplate/search/477partIA_summary.html
deleted file mode 100755 (executable)
index f5c2bc2..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-<% include( 'elements/search.html',
-                  'html_init'        => $html_init,
-                  'name'             => 'lines',
-                  'query'            => 'SELECT 1',
-                  'count_query'      => 'SELECT 1',
-                  'really_disable_download' => 1,
-                  'disable_download' => 1,
-                  'nohtmlheader'     => 1,
-                  'disable_total'    => 1,
-                  'header'           => [
-                                          'Total Connections',
-                                          '% owned loop',
-                                          '% billed to end users',
-                                          '% residential',
-                                          '% residential &gt; 200kbps',
-                                        ],
-                  'xml_elements'     => [
-                                          $xml_prefix. 'a1',
-                                          $xml_prefix. 'b1',
-                                          $xml_prefix. 'c1',
-                                          $xml_prefix. 'd1',
-                                          $xml_prefix. 'e1',
-                                        ],
-                  'fields'           => [
-                                          sub { $total_count },
-                                          sub { '100.00' },
-                                          sub { '100.00' },
-                                          sub { $total_percentage },
-                                          sub { $above_200_percentage },
-                                        ],
-              )
-%>
-<%init>
-
-my $curuser = $FS::CurrentUser::CurrentUser;
-
-die "access denied"
-  unless $curuser->access_right('List packages');
-
-my %opt = @_;
-my %search_hash = ();
-  
-for ( qw(agentnum magic state) ) {
-  $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
-}
-$search_hash{'country'} = 'US';
-$search_hash{'classnum'} = [ $cgi->param('classnum') ];
-
-my @column_option = grep { /^\d+$/ } $cgi->param('part1_column_option')
-  if $cgi->param('part1_column_option');
-
-my @row_option = grep { /^\d+$/ } $cgi->param('part1_row_option')
-  if $cgi->param('part1_row_option');
-
-my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi);
-
-my $total_count = 0;
-my $total_residential = 0;
-my $above_200 = 0;
-my $tech_code = $opt{tech_code};
-my $technology = $FS::Report::FCC_477::technology[$tech_code] || 'unknown';
-my $html_init = "<H2>Part IA $technology totals</H2>";
-my $xml_prefix = 'PartIA_'. chr(65 + $tech_code);
-
-my $not_first_row = 0; # ugh;
-foreach my $row ( @row_option ) {
-  foreach my $column ( @column_option ) {
-
-    my @report_option = ( $row || '-1', $column || '-1', $technology_option[$tech_code] );
-
-    my ( $count, $residential ) = FS::cust_pkg->fcc_477_count(
-      { %search_hash, 'report_option' => join(',', @report_option) }
-    );
-
-    $total_count += $count;
-    $total_residential += $residential;
-    $above_200 += $residential if $not_first_row;
-  }
-  $not_first_row++;
-}
-
-my $total_percentage =
-  sprintf("%.2f", $total_count ? 100*$total_residential/$total_count : 0);
-
-my $above_200_percentage =
-  sprintf("%.2f", $total_count ? 100*$above_200/$total_count : 0);
-
-
-</%init>
index d2cc8c3..95c00a3 100755 (executable)
@@ -1,17 +1,44 @@
-<% include( 'elements/search.html',
-                  'html_init'        => $html_init,
-                  'name'             => 'lines',
-                  'query'            => $query,
-                  'count_query'      => 'SELECT 11',
-                  'really_disable_download' => 1,
-                  'disable_download' => 1,
-                  'nohtmlheader'     => 1,
-                  'disable_total'    => 1,
-                  'header'           => [ @headers ],
-                  'xml_elements'     => [ @xml_elements ],
-                  'fields'           => [ @fields ],
-              )
-%>
+% if ( $cgi->param('_type') eq 'xml' ) {
+%   my @cols = qw(a b c d);
+%   for ( my $row = 0; $row < scalar(@rows); $row++ ) {
+%     for my $col (0..3) {
+%       if ( exists($data[$col][$row]) and $data[$col][$row] > 0 ) {
+<PartII_<% $row + 1 %><% $cols[$col] %>>\
+<% $data[$col][$row] %>\
+</PartII_<% $row + 1 %><% $cols[$col] %>>
+%       }
+%     } #for $col
+%   } #for $row
+% } else { # HTML mode
+% # fake up the search-html.html header
+<H2>Part IIA</H2>
+<TABLE>
+  <TR><TD VALIGN="bottom"><BR></TD></TR>
+  <TR><TD COLSPAN=2>
+  <TABLE CLASS="grid" CELLSPACING=0>
+    <TR>
+% foreach (@row1_headers) {
+      <TH><% $_ %></TH>
+% }
+    </TR>
+% my $row = 0;
+% foreach my $rowhead (@rows) {
+    <TR CLASS="row<%$row % 2%>"> 
+      <TD STYLE="text-align: left; font-weight: bold"><% $rowhead %></TD>
+%     for my $col (0..3) {
+      <TD>
+%       if ( exists($data[$col][$row]) ) {
+      <% $data[$col][$row] %>
+%       }
+      </TD>
+%     } # for $col
+    </TR>
+%   $row++;
+% } #for $rowhead
+  </TABLE>
+  </TD></TR>
+</TABLE>
+% } #XML/HTML
 <%init>
 
 my $curuser = $FS::CurrentUser::CurrentUser;
@@ -19,83 +46,76 @@ my $curuser = $FS::CurrentUser::CurrentUser;
 die "access denied"
   unless $curuser->access_right('List packages');
 
-my $html_init = '<H2>Part IIA</H2>';
 my %search_hash = ();
-  
-for ( qw(agentnum magic state) ) {
-  $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
-}
-$search_hash{'country'} = 'US';
-$search_hash{'classnum'} = [ $cgi->param('classnum') ];
-
-my @row_option = grep { /^\d+$/ } $cgi->param('part2a_row_option')
-  if $cgi->param('part2a_row_option');
-
-# fudge in two rows of LD carrier
-unshift @row_option, $row_option[0];
-
-# fudge in the first pair of rows
-unshift @row_option, '';
-unshift @row_option, '';
-
-my $query = 'SELECT '. join(' UNION SELECT ', 1..11);
 
-my $total_count = 0;
-my $column_value = sub {
-  my $row = shift;
-
-  my @report_option = ( $row_option[$row - 1] || '' );
-
-  my $sql_query = FS::cust_pkg->search(
-    { %search_hash, 'report_option' => join(',', @report_option) }
-  );
-
-  my $count_sql = delete($sql_query->{'count_query'});
-  if ( $row == 2 || $row == 4 ) {
-    $count_sql =~ s/COUNT\(\*\) FROM/sum(COALESCE(CASE WHEN cust_main.company IS NULL OR cust_main.company = '' THEN   CASE WHEN part_pkg.fcc_ds0s IS NOT NULL AND part_pkg.fcc_ds0s > 0 THEN part_pkg.fcc_ds0s WHEN pkg_class.fcc_ds0s IS NOT NULL AND pkg_class.fcc_ds0s > 0 THEN pkg_class.fcc_ds0s ELSE 0 END   ELSE 0 END, 0) ) FROM/
-      or die "couldn't parse count_sql";
-  } else {
-    $count_sql =~ s/COUNT\(\*\) FROM/sum(COALESCE(CASE WHEN part_pkg.fcc_ds0s IS NOT NULL AND part_pkg.fcc_ds0s > 0 THEN part_pkg.fcc_ds0s WHEN pkg_class.fcc_ds0s IS NOT NULL AND pkg_class.fcc_ds0s > 0 THEN pkg_class.fcc_ds0s ELSE 0 END, 0)) FROM/
-      or die "couldn't parse count_sql";
-  }
-
-  my $count_sth = dbh->prepare($count_sql)
-    or die "Error preparing $count_sql: ". dbh->errstr;
-  $count_sth->execute
-    or die "Error executing $count_sql: ". $count_sth->errstr;
-  my $count_arrayref = $count_sth->fetchrow_arrayref;
-  my $count = $count_arrayref->[0];
+$search_hash{'agentnum'} = $cgi->param('agentnum');
+$search_hash{'state'}    = $cgi->param('state');
+$search_hash{'classnum'} = [ $cgi->param('classnum') ];
+$search_hash{'status'}   = 'active';
 
-  $total_count = $count if $row == 1;
-  $count = sprintf('%.2f', $total_count ? 100*$count/$total_count : 0)
-    if $row != 1;
+my @row_option;
+foreach ($cgi->param('part2a_row_option')) {
+  push @row_option, (/^\d+$/ ? $_ : undef);
+}
 
-  return "$count";
+my $is_residential = "AND COALESCE(cust_main.company, '') = ''";
+my $has_report_option = sub {
+  map {
+    defined($row_option[$_]) ?
+    " AND EXISTS(
+      SELECT 1 FROM part_pkg_option 
+      WHERE part_pkg_option.pkgpart = part_pkg.pkgpart
+      AND optionname = 'report_option_" . $row_option[$_]."'
+      AND optionvalue = '1'
+    )" : ' AND FALSE'
+  } @_
 };
 
-my @headers = (
-  '',
-  'End user lines',
-  'UNE-P replacement',
-  'UNE (unswitched)',
-  'UNE-P',
+# an arrayref for each column
+my @data;
+# get the skeleton of the query
+my $sql_query = FS::cust_pkg->search(\%search_hash);
+my $from_where = $sql_query->{'count_query'};
+$from_where =~ s/^SELECT COUNT\(\*\) //;
+
+# for row 1
+my $query_ds0 = "SELECT SUM(COALESCE(part_pkg.fcc_ds0s, pkg_class.fcc_ds0s, 0))
+  $from_where AND fcc_voip_class = '4'"; # 4 = Local Exchange
+
+my $total_lines = FS::Record->scalar_sql($query_ds0);
+# always return zero for the number of resold lines, until an actual ILEC
+# starts using this report
+
+@data = (
+  [ $total_lines ],
+  [ 0 ],
+  [ 0 ],
+  [ 0 ],
 );
 
-my @xml_elements = (
-  sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}a" },
-  sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}b" },
-  sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}c" },
-  sub { my $row = shift; my $rownum = $row->[0] + 1; "PartII_${rownum}d" },
+my @row_conds = (
+  $is_residential,
+  $has_report_option->(0), # LD carrier
+  ($has_report_option->(0))[0] . $is_residential,
+  $has_report_option->(1..7),
 );
 
+if ( $total_lines > 0 ) {
+  foreach (@row_conds) {
+    my $sql = $query_ds0 . $_;
+    my $lines = FS::Record->scalar_sql($sql);
+    my $percent = sprintf('%.2f', 100 * $lines / $total_lines);
+    push @{ $data[0] }, $percent;
+  }
+}
 
 my @rows = (
   'lines',
   '% residential',
   '% LD carrier',
-  '% residential and LD carrier',
-  '% own loops',
-  '% obtained unswitched UNE loops',
+  '% residential and LD',
+  '% owned loops',
+  '% unswitched UNE',
   '% UNE-P',
   '% UNE-P replacement',
   '% FTTP',
@@ -103,13 +123,12 @@ my @rows = (
   '% wireless',
 );
 
-my @fields = (
-  sub { my $row = shift; $rows[$row->[0] - 1]; },
-  sub { my $row = shift; &{$column_value}($row->[0]); },
-  sub { 0; },
-  sub { 0; },
-  sub { 0; },
+my @row1_headers = (
+  '',
+  'End user lines',
+  'UNE-P replacement',
+  'unswitched UNE',
+  'UNE-P',
 );
 
-shift @fields if $cgi->param('_type') eq 'xml';
 </%init>
index c58310d..5b9b307 100755 (executable)
@@ -3,9 +3,10 @@
 %   for ( my $row = 0; $row < scalar(@rows); $row++ ) {
 %     for my $col (0..2) {
 %       if ( exists($data[$col][$row]) ) {
-<PartII_<% $row %><% $cols[$col] %>>
+<PartII_<% $row + 1 %><% $cols[$col] %>>\
+<% $data[$col][$row] %>\
+</PartII_<% $row + 1 %><% $cols[$col] %>>
 %       }
-</PartII_<% $row %><% $cols[$col] %>>
 %     } #for $col
 %   } #for $row
 % } else { # HTML mode
 <TABLE>
   <TR><TD VALIGN="bottom"><BR></TD></TR>
   <TR><TD COLSPAN=2>
-  <TABLE CLASS="grid" CELLSPACING=0 STYLE="border: 1px solid #cccccc;" BGCOLOR="#cccccc">
+  <TABLE CLASS="grid" CELLSPACING=0>
     <TR>
 % foreach (@headers) {
-      <TH class="grid"><% $_ %></TH>
+      <TH><% $_ %></TH>
 % }
     </TR>
-% my @bgcolor = ('eeeeee','ffffff');
 % my $row = 0;
 % foreach my $rowhead (@rows) {
-    <TR> 
-      <TD CLASS="grid" BGCOLOR="#<% $bgcolor[$row % 2] %>"><% $rowhead %></TD>
+    <TR CLASS="row<% $row % 2 %>"
+      <TD STYLE="text-align: left; font-weight: bold"><% $rowhead %></TD>
 %     for my $col (0..2) {
-      <TD CLASS="grid" BGCOLOR="#<% $bgcolor[$row % 2] %>">
+      <TD>
 %       if ( exists($data[$col][$row]) ) {
       <% $data[$col][$row] %>
 %       }
index 2fd5119..b2dd9ca 100755 (executable)
@@ -1,4 +1,7 @@
-<% include( 'elements/search.html',
+% if ( $cgi->param('_type') =~ /^xml$/ ) {
+<zip_code>
+% }
+<& elements/search.html,
                   'html_init'         => $html_init,
                   'name'              => 'zip code',
                   'query'             => $sql_query,
                   'url'               => $opt{url} || '',
                   'really_disable_download'  => 1,
 
-              )
-%>
+              
+&>
+% if ( $cgi->param('_type') =~ /^xml$/ ) {
+</zip_code>
+% }
 <%init>
 
 my $curuser = $FS::CurrentUser::CurrentUser;
@@ -32,8 +38,8 @@ for ( qw(agentnum magic state) ) {
 }
 $search_hash{'country'} = 'US';
 $search_hash{'classnum'} = [ $cgi->param('classnum') ];
-$search_hash{report_option} = $cgi->param('partv_report_option')
-  if $cgi->param('partv_report_option');
+$search_hash{report_option} = $cgi->param('part5_report_option')
+  if $cgi->param('part5_report_option');
 
 my $sql_query = FS::cust_pkg->search( { %search_hash,
                                         'fcc_line'    => 1,
index 8425c4b..59a6fb5 100755 (executable)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                   'html_init'       => '<H2>Part VI</H2>',
                   'html_foot'       => $html_foot,
                   'name'            => 'regions',
@@ -24,8 +24,8 @@
                   'url'             => $opt{url} || '',
                   'xml_row_element' => 'Datarow',
                   'really_disable_download' => 1,
-              )
-%>
+              
+&>
 <%init>
 
 my $curuser = $FS::CurrentUser::CurrentUser;
diff --git a/httemplate/search/agent_commission.html b/httemplate/search/agent_commission.html
new file mode 100644 (file)
index 0000000..b94ae9f
--- /dev/null
@@ -0,0 +1,197 @@
+%# still not a good way to do rows grouped by some field in a search.html 
+%# report
+% if ( $type eq 'xls' ) {
+<% $data %>\
+% } else {
+<& /elements/header.html, $title &>
+<P ALIGN="right" CLASS="noprint">
+Download full results<BR>
+as <A HREF="<% $cgi->self_url %>;_type=xls">Excel spreadsheet</A></P>
+<BR>
+<STYLE TYPE="text/css">
+td.cust_head {
+  border-left: none;
+  border-right: none;
+  padding-top: 0.5em;
+  font-weight: bold;
+  background-color: #ffffff;
+}
+td.money { text-align: right; }
+td.money:before { content: '<% $money_char %>'; }
+.row0 { background-color: #eeeeee; }
+.row1 { background-color: #ffffff; }
+</STYLE>
+<& /elements/table-grid.html &>
+  <TR STYLE="background-color: #cccccc">
+    <TH CLASS="grid">Package</TH>
+    <TH CLASS="grid">Sales</TH>
+    <TH CLASS="grid">Percentage</TH>
+    <TH CLASS="grid">Commission</TH>
+  </TR>
+% my ($custnum, $sales, $commission, $row, $bgcolor) = (0, 0, 0, 0);
+% foreach my $cust_pkg ( @cust_pkg ) {
+%   if ( $custnum ne $cust_pkg->custnum ) {
+%     # start of a new customer section
+%     my $cust_main = $cust_pkg->cust_main;
+%     my $label = $cust_main->custnum . ': '. $cust_main->name;
+%     $bgcolor = 0;
+  <TR>
+    <TD COLSPAN=4 CLASS="cust_head">
+      <A HREF="<%$p%>view/cust_main.cgi?<%$cust_main->custnum%>"><% $label %></A>
+    </TD>
+  </TR>
+%   }
+  <TR CLASS="row<% $bgcolor %>">
+    <TD CLASS="grid"><% $cust_pkg->pkg_label %></TD>
+    <TD CLASS="money"><% sprintf('%.2f', $cust_pkg->sum_charged) %></TD>
+    <TD ALIGN="right"><% $cust_pkg->percent %>%</TD>
+    <TD CLASS="money"><% sprintf('%.2f',
+                      $cust_pkg->sum_charged * $cust_pkg->percent / 100) %></TD>
+  </TR>
+%   $sales += $cust_pkg->sum_charged;
+%   $commission += $cust_pkg->sum_charged * $cust_pkg->percent / 100;
+%   $row++;
+%   $bgcolor = 1-$bgcolor;
+%   $custnum = $cust_pkg->custnum;
+% }
+  <TR STYLE="background-color: #f5f6be">
+    <TD CLASS="grid">
+      <% emt('[quant,_1,package] with commission', $row) %>
+    </TD>
+    <TD CLASS="money"><% sprintf('%.2f', $sales) %></TD>
+    <TD></TD>
+    <TD CLASS="money"><% sprintf('%.2f', $commission) %></TD>
+  </TR>
+</TABLE>
+<& /elements/footer.html &>
+% }
+<%init>
+die "access denied" 
+  unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+my ($begin, $end) = FS::UI::Web::parse_beginning_ending($cgi);
+$cgi->param('agentnum') =~ /^(\d+)$/ or die "bad agentnum";
+my $agentnum = $1;
+my $agent = FS::agent->by_key($agentnum);
+
+my $title = $agent->agent . ' commissions';
+
+my $sum_charged =
+  '(SELECT SUM(setup + recur) FROM cust_bill_pkg JOIN cust_bill USING (invnum)'.
+    'WHERE cust_bill_pkg.pkgnum = cust_pkg.pkgnum AND '.
+    "cust_bill._date >= $begin AND cust_bill._date < $end)";
+
+my @select = (
+  'cust_pkg.*',
+  'agent_pkg_class.commission_percent AS percent',
+  "$sum_charged AS sum_charged",
+);
+
+my $query = {
+  'table'       => 'cust_pkg',
+  'select'      => join(',', @select),
+  'addl_from'   => 'JOIN cust_main  USING (custnum) '.
+                   'JOIN part_pkg   USING (pkgpart) '.
+                   'JOIN agent_pkg_class ON (  '.
+                     'cust_main.agentnum = agent_pkg_class.agentnum AND '.
+                     '( agent_pkg_class.classnum = part_pkg.classnum OR '.
+                     '(agent_pkg_class IS NULL AND part_pkg.classnum IS NULL)'.
+                     ' )  ) ',
+  'extra_sql'   => "WHERE cust_main.agentnum = $agentnum AND ".
+                   'agent_pkg_class.commission_percent > 0 AND '.
+                   "$sum_charged > 0",
+  'order_by'    => 'ORDER BY cust_pkg.custnum ASC',
+};
+
+my @cust_pkg = qsearch($query);
+
+my $money_char = FS::Conf->new->config('money_char') || '$';
+
+my $data = '';
+my $type = $cgi->param('_type');
+if ( $type eq 'xls') {
+  # some false laziness with the above...
+  my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format;
+  my $filename = 'agent_commission' . $format->{extension}; 
+  http_header('Content-Type' => $format->{mime_type});
+  http_header('Content-Disposition' => qq!attachment;filename="$filename"!);
+  my $XLS = IO::Scalar->new(\$data);
+  my $workbook = $format->{class}->new($XLS);
+  my $worksheet = $workbook->add_worksheet(substr($title, 0, 31));
+
+  my $cust_head_format = $workbook->add_format(
+    bold      => 1,
+    underline => 1,
+    text_wrap => 0,
+    bg_color  => 'white',
+  );
+
+  my $col_head_format = $workbook->add_format(
+    bold      => 1,
+    align     => 'center',
+    bg_color  => 'silver'
+  );
+
+  my @format;
+  foreach (0, 1) {
+    my %bg = (bg_color => $_ ? 'white' : 'silver');
+    $format[$_] = {
+      'text'    => $workbook->add_format(%bg),
+      'money'   => $workbook->add_format(%bg, num_format => $money_char.'#0.00'),
+      'percent' => $workbook->add_format(%bg, num_format => '0.00%'),
+    };
+  }
+  my $total_format = $workbook->add_format(
+    bg_color    => 'yellow',
+    num_format  => $money_char.'#0.00',
+    top         => 1
+  );
+
+  my ($r, $c) = (0, 0);
+  foreach (qw(Package Sales Percentage Commission)) {
+    $worksheet->write($r, $c++, $_, $col_head_format);
+  }
+  $r++;
+
+  my ($custnum, $sales, $commission, $row, $bgcolor) = (0, 0, 0, 0);
+  my $label_length = 0;
+  foreach my $cust_pkg ( @cust_pkg ) {
+    if ( $custnum ne $cust_pkg->custnum ) {
+      # start of a new customer section
+      my $cust_main = $cust_pkg->cust_main;
+      my $label = $cust_main->custnum . ': '. $cust_main->name;
+      $bgcolor = 0;
+      $worksheet->set_row($r, 20);
+      $worksheet->merge_range($r, 0, $r, 3, $label, $cust_head_format);
+      $r++;
+    }
+    $c = 0;
+    my $percent = $cust_pkg->percent / 100;
+    $worksheet->write($r, $c++, $cust_pkg->pkg_label, $format[$bgcolor]{text});
+    $worksheet->write($r, $c++, $cust_pkg->sum_charged, $format[$bgcolor]{money});
+    $worksheet->write($r, $c++, $percent, $format[$bgcolor]{percent});
+    $worksheet->write($r, $c++, ($cust_pkg->sum_charged * $percent),
+                                $format[$bgcolor]{money});
+
+    $label_length = max($label_length, length($cust_pkg->pkg_label));
+    $sales += $cust_pkg->sum_charged;
+    $commission += $cust_pkg->sum_charged * $cust_pkg->percent / 100;
+    $row++;
+    $bgcolor = 1-$bgcolor;
+    $custnum = $cust_pkg->custnum;
+    $r++;
+  }
+
+  $c = 0;
+  $label_length = max($label_length, 20);
+  $worksheet->set_column($c, $c, $label_length);
+  $worksheet->write($r, $c++, mt('[quant,_1,package] with commission', $row),
+                                  $total_format);
+  $worksheet->set_column($c, $c + 2, 11);
+  $worksheet->write($r, $c++, $sales, $total_format);
+  $worksheet->write($r, $c++, '', $total_format);
+  $worksheet->write($r, $c++, $commission, $total_format);
+
+  $workbook->close;
+}
+</%init>
index ac65371..015aca4 100644 (file)
@@ -1,4 +1,4 @@
-<% include('elements/search.html',
+<& elements/search.html,
      'title'         => 'Inventory summary per agent',
      'name_singular' => 'agent',
      'query'         => { 'table'     => 'agent',
@@ -10,8 +10,7 @@
                         " AND $agentnums_sql",
      'header'        => \@header,
      'fields'        => \@fields,
-   )
-%>
+&>
 <%init>
 
 die "access denied"
index b6676f2..b740bdc 100755 (executable)
@@ -26,7 +26,7 @@ function start() {
 %     -expires => '-1d',
 %   );
 %   $r->headers_out->add( 'Set-Cookie' => $cookie->as_string );
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'         => 'Invoice Batches',
                 'name_singular' => 'batch',
                 'query'         => { 'table'     => 'bill_batch',
@@ -67,9 +67,7 @@ function start() {
                  'agent_pos' => 1,
                  'html_foot' => include('.foot'),
 
-      )
-
-%>
+&>
 %}
 <%def .foot>
 <SCRIPT type="text/javascript">
index d0d7292..ca303d3 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                'title' => $title,
                'name'  => 'call detail records',
                'query' => $query,
@@ -9,27 +9,8 @@
                'fields' => \@fields,
                'links' => \@links,
                'html_form'   => qq!<FORM NAME="cdrForm" ACTION="$p/misc/cdr.cgi" METHOD="POST">!,
-               #false laziness w/queue.html
-               'html_foot' => sub {
-                                if ( $areboxes ) {
-                                  '<BR><INPUT TYPE="button" VALUE="select all" onClick="setAll(true)">'.
-                                  '<INPUT TYPE="button" VALUE="unselect all" onClick="setAll(false)">'.
-                                  qq!<BR><INPUT TYPE="submit" NAME="action" VALUE="reprocess selected" onClick="return confirm('Are you sure you want to reprocess the selected CDRs?')">!.
-                                  qq!<INPUT TYPE="submit" NAME="action" VALUE="delete selected" onClick="return confirm('Are you sure you want to delete the selected CDRs?')"><BR>!.
-                                  '<SCRIPT TYPE="text/javascript">'.
-                                  '  function setAll(setTo) { '.
-                                  '    theForm = document.cdrForm;'.
-                                  '    for (i=0,n=theForm.elements.length;i<n;i++)'.
-                                  '      if (theForm.elements[i].name.indexOf("acctid") != -1)'.
-                                  '        theForm.elements[i].checked = setTo;'.
-                                  '  }'.
-                                  '</SCRIPT>';
-                                } else {
-                                  '';
-                                }
-                              },
-             )
-%>
+               'html_foot' => $html_foot,
+&>
 <%init>
 
 die "access denied"
@@ -44,8 +25,6 @@ my $totalminutes_sub = sub {
 
 my $conf = new FS::Conf;
 
-my $areboxes = 0;
-
 my $title = 'Call Detail Records';
 my $hashref = {};
 
@@ -355,7 +334,6 @@ my %links = (
 @fields = map { exists($fields{$_}) ? $fields{$_} : $_ } @fields;
 unshift @fields, sub {
                        return '' unless $edit_data;
-                       $areboxes = 1;
                        my $cdr = shift;
                        my $acctid = $cdr->acctid;
                        qq!<INPUT NAME="acctid$acctid" TYPE="checkbox" VALUE="1">!;
@@ -409,4 +387,14 @@ if ( $topmode ) {
     $nototalminutes = 1;
 }
 
+my $html_foot = include('/search/elements/checkbox-foot.html',
+  actions => [
+    { submit  => "reprocess selected",
+      name    => "action",
+      confirm => "Are you sure you want to reprocess the selected CDRs?" },
+    { submit  => "delete selected",
+      name    => "action",
+      confirm => "Are you sure you want to delete the selected CDRs?" },
+  ]
+);
 </%init>
index 3c0530e..473aed3 100755 (executable)
@@ -62,7 +62,7 @@
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('List invoices');
 
-my $join_cust_main = 'LEFT JOIN cust_main USING ( custnum )';
+my $join_cust_main = FS::UI::Web::join_cust_main('cust_bill');
 #here is the agent virtualization
 my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql;
 
@@ -97,7 +97,7 @@ if ( $cgi->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/ ) {
     $search{'refnum'} = $1;
   }
 
-  if ( $cgi->param('cust_classnum') ) {
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
     $search{'cust_classnum'} = [ $cgi->param('cust_classnum') ];
   }
 
@@ -198,7 +198,6 @@ if ( $cgi->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/ ) {
   };
 
 }
-
 my $link  = [ "${p}view/cust_bill.cgi?", 'invnum', ];
 my $clink = sub {
   my $cust_bill = shift;
index 90c8913..9fb533a 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'       => $title,
                  'html_init'   => $html_init,
                  'menubar'     => $menubar,
@@ -60,8 +60,8 @@
                               '',
                               FS::UI::Web::cust_styles(),
                             ],
-             )
-%>
+             
+&>
 <%init>
 
 my $curuser = $FS::CurrentUser::CurrentUser;
@@ -100,7 +100,7 @@ my $where = 'WHERE '. FS::cust_bill_event->search_sql_where( \%search );
 
 my $join = 'LEFT JOIN part_bill_event USING ( eventpart ) '.
            'LEFT JOIN cust_bill       USING ( invnum    ) '.
-           'LEFT JOIN cust_main       USING ( custnum   ) ';
+           FS::UI::Web::join_cust_main('cust_bill');
 
 my $sql_query = {
   'table'     => 'cust_bill_event',
index 79de749..ff20458 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                 'title'       => $title,
                 'name'        => 'net payments',
                 'query'       => $sql_query,
@@ -71,8 +71,8 @@
                              '',
                              FS::UI::Web::cust_styles(),
                            ],
-          )
-%>
+          
+&>
 <%init>
 
 die "access denied"
@@ -99,9 +99,12 @@ if ( $cgi->param('refnum') && $cgi->param('refnum') =~ /^(\d+)$/ ) {
   $title = $part_referral->referral. " $title";
 }
 
-if ( $cgi->param('cust_classnum') ) {
-  my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
-  push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, prepaid_income.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+  my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+  push @search, 'COALESCE( cust_main.classnum, 0) IN ( '.
+                    join(',', map { $_ || '0' } @classnums ).
+                ' )'
     if @classnums;
 }
 
@@ -117,8 +120,8 @@ my $where = 'WHERE '. join(' AND ', @search);
 #
 my $count_query = 'SELECT COUNT(*), SUM(amount)
                    FROM cust_bill_pay
-                     LEFT JOIN cust_bill USING ( invnum  )
-                     LEFT JOIN cust_main USING ( custnum ) '.
+                     LEFT JOIN cust_bill USING ( invnum  ) '.
+                     FS::UI::Web::join_cust_main('cust_bill') .
                   $where;
 
 my $sql_query   = {
@@ -137,8 +140,8 @@ my $sql_query   = {
   'hashref'   => {},
   'extra_sql' => $where,
   'addl_from' => 'LEFT JOIN cust_bill   USING ( invnum  )
-                  LEFT JOIN cust_pay    USING ( paynum )
-                  LEFT JOIN cust_main ON ( cust_bill.custnum = cust_main.custnum )',
+                  LEFT JOIN cust_pay    USING ( paynum ) '.
+                  FS::UI::Web::join_cust_main('cust_bill')
 };
 
 my $cust_bill_link = sub {
index 1e67e93..3a3b0fe 100644 (file)
@@ -222,9 +222,9 @@ if ( $conf->exists('enable_taxclasses') ) {
 
 # valid in both the tax and non-tax cases
 my $join_cust = 
-  " LEFT JOIN cust_bill USING (invnum)
-    LEFT JOIN cust_main USING (custnum)
-  ";
+  " LEFT JOIN cust_bill USING (invnum)".
+  # use cust_pkg.locationnum if it exists
+  FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg');
 
 #agent virtualization
 my $agentnums_sql =
@@ -260,13 +260,16 @@ if ( $cgi->param('refnum') =~ /^(\d+)$/ ) {
   push @where, "cust_main.refnum = $1";
 }
 
-# cust_classnum
-if ( $cgi->param('cust_classnum') ) {
-  my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
-  push @where, 'cust_main.classnum IN('.join(',',@classnums).')'
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+  my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+  push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+                   join(',', map { $_ || '0' } @classnums ).
+               ' )'
     if @classnums;
 }
 
+
 # custnum
 if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
   push @where, "cust_main.custnum = $1";
@@ -278,11 +281,11 @@ my $join_pkg =
   LEFT JOIN part_pkg      USING (pkgpart)';
 
 my $part_pkg = 'part_pkg';
-if ( $cgi->param('use_override') ) {
+if ( $cgi->param('use_override') ) { #"Separate sub-packages from parents"
   # still need the real part_pkg for tax applicability, 
   # so alias this one
   $join_pkg .= " LEFT JOIN part_pkg AS override ON (
-  COALESCE(cust_bill_pkg.pkgpart_override, cust_pkg.pkgpart, 0) = part_pkg.pkgpart
+  COALESCE(cust_bill_pkg.pkgpart_override, cust_pkg.pkgpart, 0) = override.pkgpart
   )";
   $part_pkg = 'override';
 }
@@ -559,12 +562,11 @@ if ( $cgi->param('nottax') ) {
 
 
 #total payments
-my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount) AS pay_amount,
-    billpkgnum
-  FROM cust_bill_pay_pkg
-  GROUP BY billpkgnum";
-$join_pkg .= " LEFT JOIN ($pay_sub) AS item_pay USING (billpkgnum)";
-push @select, 'item_pay.pay_amount';
+my $pay_sub = "SELECT SUM(cust_bill_pay_pkg.amount)
+                 FROM cust_bill_pay_pkg
+                   WHERE cust_bill_pkg.billpkgnum = cust_bill_pay_pkg.billpkgnum
+              ";
+push @select, "($pay_sub) AS pay_amount";
 
 
 # credit
@@ -630,13 +632,12 @@ if ( $cgi->param('credit') ) {
 
   #still want a credit total column
 
-  my $credit_sub = "SELECT SUM(cust_credit_bill_pkg.amount) AS credit_amount,
-      billpkgnum
-    FROM cust_credit_bill_pkg
-    GROUP BY billpkgnum";
-  $join_pkg .= " LEFT JOIN ($credit_sub) AS item_credit USING (billpkgnum)";
-
-  push @select,   'item_credit.credit_amount';
+  my $credit_sub = "
+    SELECT SUM(cust_credit_bill_pkg.amount)
+      FROM cust_credit_bill_pkg
+        WHERE cust_bill_pkg.billpkgnum = cust_credit_bill_pkg.billpkgnum
+  ";
+  push @select, "($credit_sub) AS credit_amount";
 
 }
 
@@ -647,7 +648,7 @@ $where &&= "WHERE $where";
 
 my $query = {
   'table'     => 'cust_bill_pkg',
-  'addl_from' => "$join_cust $join_pkg",
+  'addl_from' => "$join_pkg $join_cust",
   'hashref'   => {},
   'select'    => join(",\n", @select ),
   'extra_sql' => $where,
@@ -656,7 +657,7 @@ my $query = {
 
 my $count_query =
   'SELECT ' . join(',', @total) .
-  " FROM cust_bill_pkg $join_cust $join_pkg
+  " FROM cust_bill_pkg $join_pkg $join_cust
   $where";
 
 @peritem_desc = map {emt($_)} @peritem_desc;
index bb8038a..f598341 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'       => 'Discounts',
                  'name'        => 'discounts',
                  'query'       => $query,
@@ -68,8 +68,8 @@
                               '',
                               FS::UI::Web::cust_styles(),
                             ],
-           )
-%>
+           
+&>
 <%init>
 
 #a little false laziness below w/cust_bill_pkg.cgi
@@ -127,12 +127,12 @@ my $join_cust_pkg_discount =
   'LEFT JOIN cust_pkg_discount USING (pkgdiscountnum)';
 
 my $join_cust =
-  '      JOIN cust_bill_pkg USING ( billpkgnum )
-         JOIN cust_bill USING ( invnum ) 
-    LEFT JOIN cust_main USING ( custnum ) ';
+  '         JOIN cust_bill USING ( invnum ) '.
+  FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg');
 
 my $join_pkg =
-  ' LEFT JOIN cust_pkg ON ( cust_bill_pkg.pkgnum = cust_pkg.pkgnum )
+  '    JOIN cust_bill_pkg USING ( billpkgnum )
+  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 ';
@@ -140,7 +140,7 @@ my $join_pkg =
 my $where = ' WHERE '. join(' AND ', @where);
 
 $count_query .=
-  " FROM cust_bill_pkg_discount $join_cust_pkg_discount $join_cust $join_pkg ".
+  " FROM cust_bill_pkg_discount $join_cust_pkg_discount $join_pkg $join_cust ".
   $where;
 
 my @select = (
@@ -155,7 +155,7 @@ push @select, 'cust_main.custnum',
 
 my $query = {
   'table'     => 'cust_bill_pkg_discount',
-  'addl_from' => "$join_cust_pkg_discount $join_cust $join_pkg",
+  'addl_from' => "$join_cust_pkg_discount $join_pkg $join_cust",
   'hashref'   => {},
   'select'    => join(', ', @select ),
   'extra_sql' => $where,
index 1289ff7..c4dde32 100644 (file)
@@ -156,9 +156,13 @@ if ( @refnum ) {
   push @where, 'cust_main.refnum IN ('.join(',', @refnum).')';
 }
 
-my @cust_classnums = grep /^\d+$/, $cgi->param('cust_classnum');
-if ( @cust_classnums ) {
-  push @where, 'cust_main.classnum IN ('.join(',', @cust_classnums).')';
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+  my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+  push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+                   join(',', map { $_ || '0' } @classnums ).
+               ' )'
+    if @classnums;
 }
 
 if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
index f5d8fa1..cabf8c0 100755 (executable)
@@ -103,9 +103,13 @@ if ( $cgi->param('refnum') && $cgi->param('refnum') =~ /^(\d+)$/ ) {
   $title = $part_referral->referral. " $title";
 }
 
-if ( $cgi->param('cust_classnum') ) {
-  my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
-  push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+  my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+  push @search, 'COALESCE( cust_main.classnum, 0) IN ( '.
+                    join(',', map { $_ || '0' } @classnums ).
+                ' )'
     if @classnums;
 }
 
@@ -137,7 +141,7 @@ my $where = 'WHERE '. join(' AND ', @search);
 
 my $count_query = 'SELECT COUNT(*), SUM(amount) ';
 $count_query .= ', SUM(' . FS::cust_credit->unapplied_sql . ') ' if $unapplied;
-$count_query .= 'FROM cust_credit LEFT JOIN cust_main USING ( custnum ) '.
+$count_query .= 'FROM cust_credit'. FS::UI::Web::join_cust_main('cust_credit').
                   $where;
 
 my @count_addl = ( $money_char.'%.2f total credited (gross)' );
@@ -148,7 +152,7 @@ my $sql_query   = {
   'select'    => join(', ',@select),
   'hashref'   => {},
   'extra_sql' => $where,
-  'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+  'addl_from' => FS::UI::Web::join_cust_main('cust_credit')
 };
 
 </%init>
index 9fd6a98..88f897d 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                 'title'       => $title,
                 'name'        => 'net credits',
                 'query'       => $sql_query,
@@ -64,8 +64,8 @@
                              '',
                              FS::UI::Web::cust_styles(),
                            ],
-          )
-%>
+          
+&>
 <%init>
 
 die "access denied"
@@ -103,8 +103,8 @@ my $where = 'WHERE '. join(' AND ', @search);
 #
 my $count_query = 'SELECT COUNT(*), SUM(amount)
                    FROM cust_credit_bill
-                     LEFT JOIN cust_bill USING ( invnum  )
-                     LEFT JOIN cust_main USING ( custnum ) '.
+                     LEFT JOIN cust_bill USING ( invnum  ) '.
+                  FS::UI::Web::join_cust_main('cust_bill') .
                   $where;
 
 my $sql_query   = {
@@ -121,8 +121,8 @@ my $sql_query   = {
   'hashref'   => {},
   'extra_sql' => $where,
   'addl_from' => 'LEFT JOIN cust_bill   USING ( invnum  )
-                  LEFT JOIN cust_credit USING ( crednum )
-                  LEFT JOIN cust_main ON ( cust_bill.custnum = cust_main.custnum )',
+                  LEFT JOIN cust_credit USING ( crednum )'.
+                  FS::UI::Web::join_cust_main('cust_bill')
 };
 
 my $cust_bill_link = sub {
index 06fd881..63d70c2 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
               'title'         => 'Credit application detail', #to line item
               'name_singular' => 'credit application',
               'query'         => $query,
@@ -16,6 +16,7 @@
 
                    # line item
                    'Description',
+                   'Location',
                    @post_desc_header,
 
                    #invoice
@@ -35,6 +36,7 @@
                            ? $_[0]->get('pkg')      # possibly use override.pkg
                            : $_[0]->get('itemdesc') # but i think this correct
                        },
+                   $location_sub,
                    @post_desc,
                    'invnum',
                    sub { time2str('%b %d %Y', shift->_date ) },
@@ -46,6 +48,7 @@
                    '', #'otaker',
                    '', #reason
                    '', #line item description
+                   '', #location
                    @post_desc_null,
                    'invnum',
                    '_date',
@@ -57,6 +60,7 @@
                    '',
                    '',
                    '',
+                   '',
                    @post_desc_null,
                    $ilink,
                    $ilink,
@@ -64,7 +68,7 @@
                          FS::UI::Web::cust_header()
                    ),
                ],
-               'align' => 'rrlll'.
+               'align' => 'rrllll'.
                           $post_desc_align.
                           'rr'.
                           FS::UI::Web::cust_aligns(),
@@ -74,6 +78,7 @@
                               '',
                               '',
                               '',
+                              '',
                               @post_desc_null,
                               '',
                               '',
                               '',
                               '',
                               '',
+                              '',
                               @post_desc_null,
                               '',
                               '',
                               FS::UI::Web::cust_styles(),
                           ],
-           )
-%>
+           
+&>
 <%init>
 
 #LOTS of false laziness below w/cust_bill_pkg.cgi
@@ -377,8 +383,8 @@ my $count_query = "SELECT COUNT(DISTINCT creditbillpkgnum),
                           SUM(cust_credit_bill_pkg.amount)";
 
 my $join_cust =
-  '      JOIN cust_bill ON ( cust_bill_pkg.invnum = cust_bill.invnum ) 
-    LEFT JOIN cust_main ON ( cust_bill.custnum = cust_main.custnum ) ';
+  '      JOIN cust_bill ON ( cust_bill_pkg.invnum = cust_bill.invnum )'.
+  FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg');
 
 
 my $join_pkg;
@@ -423,10 +429,9 @@ if ( $cgi->param('nottax') ) {
     s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g for @where;
   }
 
-} else { 
+} else {
 
-  #die?
-  warn "neiether nottax nor istax parameters specified";
+  #warn "neither nottax nor istax parameters specified";
   #same as before?
   $join_pkg =  ' LEFT JOIN cust_pkg USING ( pkgnum )
                  LEFT JOIN part_pkg USING ( pkgpart ) ';
@@ -459,7 +464,7 @@ my @post_desc_header = ();
 my @post_desc = ();
 my @post_desc_null = ();
 my $post_desc_align = '';
-if ( $conf->exists('enable_taxclasses') ) {
+if ( $conf->exists('enable_taxclasses') && ! $cgi->param('istax') ) {
   push @post_desc_header, 'Tax class';
   push @post_desc, 'taxclass';
   push @post_desc_null, '';
@@ -485,4 +490,57 @@ my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
 my $conf = new FS::Conf;
 my $money_char = $conf->config('money_char') || '$';
 
+my $tax_pkg_address = $conf->exists('tax-pkg_address');
+my $tax_ship_address = $conf->exists('tax-ship_address');
+
+my $location_sub = sub {
+  #my $cust_credit_bill_pkg = shift;
+  my $self = shift;
+  my $tax_Xlocation = $self->cust_bill_pkg_tax_Xlocation;
+  if ( defined($tax_Xlocation) && $tax_Xlocation ) {
+
+    if ( ref($tax_Xlocation) eq 'FS::cust_bill_pkg_tax_location' ) {
+
+      if ( $tax_Xlocation->taxtype eq 'FS::cust_main_county' ) {
+        my $cust_main_county = $tax_Xlocation->cust_main_county;
+        if ( $cust_main_county ) {
+          $cust_main_county->label;
+        } else {
+          ''; #cust_main_county record is gone... history?  yuck.
+        }
+      } else {
+        '(CCH tax_rate)'; #XXX FS::tax_rate.. vendor taxes not yet handled here
+      }
+
+    } elsif ( ref($tax_Xlocation) eq 'FS::cust_bill_pkg_tax_rate_location' ) {
+      '(CCH)'; #XXX vendor taxes not yet handled here
+    } else {
+      'unknown tax_Xlocation '. ref($tax_Xlocation);
+    }
+
+  } else {
+
+    my $cust_bill_pkg = $self->cust_bill_pkg;
+    if ( $cust_bill_pkg->pkgnum > 0 ) {
+      my $cust_pkg = $cust_bill_pkg->cust_pkg;
+      if ( $tax_pkg_address && (my $cust_location = $cust_pkg->cust_location) ){
+        $cust_location->county_state_country;
+      } else {
+        my $cust_main = $cust_pkg->cust_main;
+        if ( $tax_ship_address && $cust_main->has_ship_address ) {
+          $cust_main->county_state_country('ship_');
+        } else {
+          $cust_main->county_state_country;
+        }
+      }
+
+    } else {
+      #tax?  we shouldn't have wound up here then...
+      ''; #return customer ship or bill address? (depending on tax-ship_address)
+    }
+
+  }
+
+};
+
 </%init>
index 75138e9..8174200 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                 'title'       => $title,
                 'name'        => 'net refunds',
                 'query'       => $sql_query,
@@ -57,8 +57,8 @@
                              '',
                              FS::UI::Web::cust_styles(),
                            ],
-          )
-%>
+          
+&>
 <%init>
 
 die "access denied"
@@ -85,9 +85,12 @@ if ( $cgi->param('refnum') && $cgi->param('refnum') =~ /^(\d+)$/ ) {
   $title = $part_referral->referral. " $title";
 }
 
-if ( $cgi->param('cust_classnum') ) {
-  my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
-  push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+  my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+  push @search, 'COALESCE( cust_main.classnum, 0) IN ( '.
+                    join(',', map { $_ || '0' } @classnums ).
+                ' )'
     if @classnums;
 }
 
@@ -103,8 +106,8 @@ my $where = 'WHERE '. join(' AND ', @search);
 #
 my $count_query = 'SELECT COUNT(*), SUM(cust_credit_refund.amount)
                    FROM cust_credit_refund
-                     LEFT JOIN cust_credit USING ( crednum )
-                     LEFT JOIN cust_main   USING ( custnum ) '.
+                     LEFT JOIN cust_credit USING ( crednum ) '.
+                  FS::UI::Web::join_cust_main('cust_credit') .
                   $where;
 
 my $sql_query   = {
@@ -121,8 +124,8 @@ my $sql_query   = {
   'hashref'   => {},
   'extra_sql' => $where,
   'addl_from' => 'LEFT JOIN cust_credit USING ( crednum   )
-                  LEFT JOIN cust_refund USING ( refundnum )
-                  LEFT JOIN cust_main ON ( cust_credit.custnum = cust_main.custnum )',
+                  LEFT JOIN cust_refund USING ( refundnum )'.
+                  FS::UI::Web::join_cust_main('cust_credit')
 };
 
 #my $cust_credit_link = sub {
index deb34b9..bfc5f43 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'       => $title,
                  'html_init'   => $html_init,
                  'menubar'     => $menubar,
@@ -62,8 +62,7 @@
                               #'',
                               FS::UI::Web::cust_styles(),
                             ],
-             )
-%>
+&>
 <%once>
 
 my $status_sub = sub { 
@@ -175,7 +174,13 @@ $search{'ending'}    = $ending;
 
 my $where = ' WHERE '. FS::cust_event->search_sql_where( \%search );
 
-my $join = FS::cust_event->join_sql();
+my $join = FS::cust_event->join_sql() .
+  'LEFT JOIN cust_location bill_location '.
+  'ON (cust_main.bill_locationnum = bill_location.locationnum) '.
+  'LEFT JOIN cust_location ship_location '.
+  'ON (cust_main.ship_locationnum = ship_location.locationnum)';
+  # warning: does not show the true service address for package events.
+  # the query to do that would be painfully slow.
 
 my $sql_query = {
   'table'     => 'cust_event',
index 08800d4..f5f8c8f 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'       => 'Zip code Search Results',
                  'name'        => 'zip codes',
                  'query'       => $sql_query,
@@ -6,8 +6,7 @@
                  'header'      => [ 'Zip code', 'Customers', ],
                  'fields'      => [ 0, 1 ],
                  'links'       => [ '', $link  ],
-             )
-%>
+&>
 <%init>
 
 die "access denied"
index 8e3c813..2c09c69 100755 (executable)
 %      my $pkg_rowspan = shift @pkg_rowspans;
 
         <% $n1 %><TD CLASS="grid" BGCOLOR="<% $bgcolor %>"  ROWSPAN="<% $pkg_rowspan%>">
-            <A HREF="<% $pkgview %>"><FONT SIZE=-1><% $pkg_comment %></FONT></A>
+            <A HREF="<% $pkgview %>"><FONT SIZE=-1><% $pkg_comment |h %></FONT></A>
         </TD>
 
 %       my $n2 = '';
index 8b39ea9..24348ff 100755 (executable)
@@ -42,10 +42,11 @@ my %search_hash = ();
 #scalars
 my @scalars = qw (
   agentnum status address zip paydate_year paydate_month invoice_terms
-  no_censustract with_geocode with_email no_POST
+  no_censustract with_geocode with_email POST no_POST
   custbatch usernum
   cancelled_pkgs
   cust_fields flattened_pkgs
+  all_tags
 );
 
 for my $param ( @scalars ) {
index 800df87..9f9eb30 100755 (executable)
@@ -1,4 +1,4 @@
-<% include('elements/search.html',
+<& elements/search.html,
               'title'       => 'Batch payment details',
               'name'        => 'batch details',
              'query'       => $sql_query,
@@ -7,55 +7,41 @@
               'disable_download' => 1,
              'header'      => [ '#',
                                 'Inv #',
-                                'Customer',
+                                 'Cust #',
                                 'Customer',
                                 'Card Name',
                                 'Card',
                                 'Exp',
                                 'Amount',
                                 'Status',
+                                 '', # error_message
                               ],
-             'fields'      => [ sub {
-                                  shift->[0];
-                                },
-                                sub {
-                                  shift->[1];
-                                },
-                                sub {
-                                  shift->[2];
-                                },
-                                sub {
-                                  my $cpb = shift;
-                                  $cpb->[3] . ', ' . $cpb->[4];
-                                },
-                                sub {
-                                  shift->[5];
-                                },
-                                sub {
-                                  my $cardnum = shift->[6];
-                                   'x'x(length($cardnum)-4). substr($cardnum,(length($cardnum)-4));
-                                },
-                                sub {
-                                  shift->[7] =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
-                                   my( $mon, $year ) = ( $2, $1 );
-                                   $mon = "0$mon" if length($mon) == 1;
-                                   "$mon/$year";
-                                },
-                                sub {
-                                  shift->[8];
-                                },
-                                sub {
-                                  shift->[9];
-                                },
-                              ],
-             'align'       => 'lllllllrl',
-             'links'       => [ ['', sub{'#';}],
-                                ["${p}view/cust_bill.cgi?", sub{shift->[1];},],
-                                ["${p}view/cust_main.cgi?", sub{shift->[2];},],
-                                ["${p}view/cust_main.cgi?", sub{shift->[2];},],
+              'fields'      => [  'paybatchnum',
+                                  'invnum',
+                                  'custnum',
+                                  sub { $_[0]->cust_main->name_short },
+                                  'payname',
+                                  'mask_payinfo',
+                                  sub {
+                                    return('') if $_[0]->payby ne 'CARD';
+                                    $_[0]->get('exp') =~ /^\d\d(\d\d)-(\d\d)/;
+                                    sprintf('%02d/%02d',$1,$2);
+                                  },
+                                  sub {
+                                    sprintf('%.02f', $_[0]->amount)
+                                  },
+                                  'status',
+                                  'error_message',
+                                ],
+             'align'       => 'rrrlllcrll',
+             'links'       => [ '',
+                                ["${p}view/cust_bill.cgi?", 'invnum'],
+                                (["${p}view/cust_main.cgi?", 'custnum']) x 2,
                               ],
-      )
-%>
+              'link_onclicks' => [ ('') x 8,
+                                   $sub_receipt
+                                 ],
+&>
 <%init>
 
 my $conf = new FS::Conf;
@@ -101,7 +87,7 @@ if ( $cgi->param('payby') ) {
 }
 
 if ( not $cgi->param('dcln') ) {
-  push @search, "cpb.status IS DISTINCT FROM 'Approved'";
+  push @search, "cust_pay_batch.status IS DISTINCT FROM 'Approved'";
 }
 
 my ($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
@@ -119,18 +105,30 @@ push @search, $curuser->agentnums_sql({ table      => 'pay_batch',
 
 my $search = ' WHERE ' . join(' AND ', @search);
 
-$count_query = 'SELECT COUNT(*) FROM cust_pay_batch AS cpb ' .
+$count_query = 'SELECT COUNT(*) FROM cust_pay_batch ' .
                   'LEFT JOIN cust_main USING ( custnum ) ' .
                   'LEFT JOIN pay_batch USING ( batchnum )' .
                  $search;
 
-#grr
-$sql_query = "SELECT paybatchnum,invnum,custnum,cpb.last,cpb.first," .
-             "cpb.payname,cpb.payinfo,cpb.exp,amount,cpb.status " .
-            "FROM cust_pay_batch AS cpb " .
-             'LEFT JOIN cust_main USING ( custnum ) ' .
-             'LEFT JOIN pay_batch USING ( batchnum ) ' .
-             "$search ORDER BY $orderby";
+$sql_query = {
+  'table'     => 'cust_pay_batch',
+  'select'    => 'cust_pay_batch.*, cust_main.*, cust_pay.paynum',
+  'hashref'   => {},
+  'addl_from' => 'LEFT JOIN pay_batch USING ( batchnum ) '.
+                 'LEFT JOIN cust_main USING ( custnum ) '.
+                 
+                 'LEFT JOIN cust_pay  USING ( batchnum, custnum ) ',
+  'extra_sql' => $search,
+  'order_by'  => "ORDER BY $orderby",
+};
+
+my $sub_receipt = sub {
+  my $paynum = shift->paynum or return '';
+  include('/elements/popup_link_onclick.html',
+    'action'  => $p.'view/cust_pay.html?link=popup;paynum='.$paynum,
+    'actionlabel' => emt('Payment Receipt'),
+  );
+};
 
 my $html_init = '';
 if ( $pay_batch ) {
index 887ec60..110da91 100755 (executable)
@@ -9,6 +9,7 @@
                                      emt('Package'),
                                      emt('Class'),
                                      emt('Status'),
+                                     emt('Ordered by'),
                                      emt('Setup'),
                                      emt('Base Recur'),
                                      emt('Freq.'),
@@ -34,6 +35,7 @@
                     sub { $_[0]->pkg; },
                     'classname',
                     sub { ucfirst(shift->status); },
+                    'otaker',
                     sub { sprintf( $money_char.'%.2f',
                                    shift->part_pkg->option('setup_fee'),
                                  );
                     '',
                     '',
                     '',
+                    '',
                     FS::UI::Web::cust_colors(),
                     '',
                   ],
-                  'style' => [ '', '', '', '', 'b', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
+                  'style' => [ '', '', '', '', 'b', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
                                FS::UI::Web::cust_styles() ],
                   'size'  => [ '', '', '', '', '-1' ],
-                  'align' => 'rrlccrrlrrrrrrrrrrl'. FS::UI::Web::cust_aligns(). 'r',
+                  'align' => 'rrlcccrrlrrrrrrrrrrl'. FS::UI::Web::cust_aligns(). 'r',
                   'links' => [
                     $link,
                     $link,
                     '',
                     '',
                     '',
+                    '',
                     '', # link to changed-from package?
                     '',
                     '',
index d70c311..23af180 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                   'title'       => 'Package discounts', 
                   'name'        => 'discounts',
                   'query'       => $query,
@@ -50,8 +50,8 @@
                                      '',
                                      FS::UI::Web::cust_styles(),
                                    ],
-           )
-%>
+           
+&>
 <%init>
 
 die "access denied"
@@ -92,8 +92,8 @@ my $count_query = "SELECT COUNT(*), SUM(amount)";
 
 my $join = ' LEFT JOIN discount  USING ( discountnum )
              LEFT JOIN cust_pkg  USING ( pkgnum )
-             LEFT JOIN part_pkg  USING ( pkgpart )
-             LEFT JOIN cust_main USING ( custnum ) ';
+             LEFT JOIN part_pkg  USING ( pkgpart ) '.
+             FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
 
 my $where = ' WHERE '. join(' AND ', @where);
 
index 9c5b32f..cdc7035 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
               'title'       => $part_svc->svc.' services in package #'.$pkgnum,
              'name'        => 'services',
               'html_form'   => $html_form,
@@ -30,8 +30,8 @@
                            ('')x4,
                          ],
               'html_foot' => sub { $areboxes ? $html_foot : '' }
-          )
-%>
+          
+&>
 <%init>
 
 die "access denied"
index 2adcbd7..e2a83b7 100644 (file)
@@ -62,7 +62,7 @@ if ( length( $cgi->param('search_svc') ) ) {
 
   my $addl_from = ' LEFT JOIN part_svc  USING ( svcpart ) '.
                   ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
-                  ' LEFT JOIN cust_main USING ( custnum ) ';
+                  FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
 
   my @extra_sql = ();
 
index 9254765..6125a1c 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
               'title'         => $title,
               'name_singular' => 'tax adjustment',
               'query'         => $query,
@@ -12,9 +12,8 @@
                                        },
                                  ],
               'links'         => [ '', '', '', $ilink ],
-          )
-%>
-
+          
+&>
 <%init>
 
 die "access denied"
index 3704b20..005d77c 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'       => 'Legacy tax exemptions',
                  'name'        => 'legacy tax exemptions',
                  'query'       => $query,
                               '',
                               FS::UI::Web::cust_styles(),
                             ],
-           )
-%>
+           
+&>
 <%init>
 
-my $join_cust = "
-    LEFT JOIN cust_main USING ( custnum )
-";
+my $join_cust = FS::UI::Web::join_cust_main('cust_tax_exempt');
 
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('View customer tax exemptions');
index 1b767f8..40b9ed7 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'       => 'Tax exemptions',
                  'name'        => 'tax exemptions',
                  'query'       => $query,
                               '',
                               FS::UI::Web::cust_styles(),
                             ],
-           )
-%>
+&>
 <%once>
 
 my $join_cust = "
-    JOIN cust_bill USING ( invnum )
-    LEFT JOIN cust_main USING ( custnum )
-";
+    JOIN cust_bill USING ( invnum )" .
+    FS::UI::Web::join_cust_main('cust_bill', 'cust_pkg');
 
 my $join_pkg = "
     LEFT JOIN cust_pkg USING ( pkgnum )
@@ -93,8 +91,8 @@ my $join_pkg = "
 
 my $join = "
     JOIN cust_bill_pkg USING ( billpkgnum )
-    $join_cust
     $join_pkg
+    $join_cust
 ";
 
 </%once>
index 12c8962..b48ff21 100644 (file)
@@ -142,8 +142,6 @@ $title .=  $sel_part_referral->referral.' '
 
 $title .= 'Customer Accounting Summary Report';
 
-my @cust_classnums = grep /^\d+$/, $cgi->param('cust_classnum');
-
 my @items  = ('netsales', 'cashflow');
 my @params = ( [], [] );
 my $setuprecur = '';
@@ -173,7 +171,7 @@ foreach (qw(agentnum refnum status)) {
   }
 }
 $search_hash{'classnum'} = [ $cgi->param('cust_classnum') ] 
-  if $cgi->param('cust_classnum');
+  if grep { $_ eq 'cust_classnum' } $cgi->param;
 
 my $query = FS::cust_main::Search->search(\%search_hash);
 my @custs = qsearch($query);
diff --git a/httemplate/search/elements/checkbox-foot.html b/httemplate/search/elements/checkbox-foot.html
new file mode 100644 (file)
index 0000000..be1caab
--- /dev/null
@@ -0,0 +1,86 @@
+<%doc>
+<& /elements/search.html,
+  # options...
+  html_foot => include('elements/checkbox-foot.html',
+                  actions => [
+                    { label   => 'Edit selected packages',
+                      action  => 'popup_package_edit()',
+                    },
+                    { submit  => 'Delete selected packages',
+                      confirm => 'Really delete these packages?'
+                    },
+                  ],
+                  filter        => '.name = "pkgpart"', # see below
+               ),
+&>
+
+This creates a footer for a search page containing a column of checkboxes.
+Typically this is used to select several items from the search result and 
+apply some change to all of them at once.  The footer always provides 
+"select all" and "unselect all" buttons.
+
+"actions" is an arrayref of action buttons to show.  Each element of the
+array is a hashref of either:
+
+- "submit" and, optionally, "confirm".  Creates a submit button.  The value 
+of "submit" becomes the "value" property of the button (and thus its label).
+If "confirm" is specified, the button will have an onclick handler that 
+displays the value of "confirm" in a popup message box and asks the user to 
+confirm the choice.
+
+- "onclick" and "label".  Creates a non-submit button that executes the 
+Javascript code in "onclick".  "label" is used as the text of the button.
+
+If you want only a single action, you can forget the arrayref-of-hashrefs
+business and just put "submit" and "confirm" (or "onclick" and "label") 
+elements in the argument list.
+
+"filter" is a javascript expression to limit which checkboxes are included in
+the "select/unselect all" actions.  By default, any input with type="checkbox"
+will be included.  If this option is given, it will be evaluated with the 
+HTML node in a variable named "obj".  The expression should return true or
+false.
+
+</%doc>
+<DIV ID="checkbox_footer" STYLE="display:block">
+<INPUT TYPE="button" VALUE="<% emt('select all') %>" onclick="setAll(true)">
+<INPUT TYPE="button" VALUE="<% emt('unselect all') %>" onclick="setAll(false)">
+<BR>
+% foreach my $action (@$actions) {
+%   if ( $action->{onclick} ) {
+<INPUT TYPE="button" <% $action->{name} %> onclick="<% $opt{onclick} %>"\
+  VALUE="<% $action->{label} |h%>">
+%   } elsif ( $action->{submit} ) {
+<INPUT TYPE="submit" <% $action->{name} %> <% $action->{confirm} %>\
+  VALUE="<% $action->{submit} |h%>">
+%   } # else do nothing
+% } #foreach
+</DIV>
+<SCRIPT>
+var checkboxes = [];
+var inputs = document.getElementsByTagName('input');
+for (var i = 0; i < inputs.length; i++) {
+  var obj = inputs[i];
+  if ( obj.type == "checkbox" && <% $filter %> ) {
+    checkboxes.push(obj);
+  }
+}
+%# avoid the need for "$areboxes" late-evaluation hackery
+if ( checkboxes.length == 0 ) {
+  document.getElementById('checkbox_footer').style.display = 'none';
+}
+function setAll(setTo) {
+  for (var i = 0; i < checkboxes.length; i++) {
+    checkboxes[i].checked = setTo;
+  }
+}
+</SCRIPT>
+<%init>
+my %opt = @_;
+my $actions = $opt{'actions'} || [ \%opt ];
+foreach (@$actions) {
+  $_->{confirm} &&= qq!onclick="return confirm('! . $_->{confirm} . qq!')"!;
+  $_->{name} &&= qq!NAME="! . $_->{name} . qq!"!;
+}
+my $filter = $opt{filter} || 'true';
+</%init>
index eb75664..cf2d495 100644 (file)
@@ -162,6 +162,15 @@ if ( grep { $cgi->param('status') eq $_ } FS::cust_main->statuses() ) {
   push @where, FS::cust_main->$method();
 }
 
+# cust_classnum (false laziness w/prepaid_income.html, elements/cust_pay_or_refund.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+  my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+  push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+                   join(',', map { $_ || '0' } @classnums ).
+               ' )'
+    if @classnums;
+}
+
 #here is the agent virtualization
 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql;
 
@@ -172,10 +181,11 @@ my $count_sql = "select count(*) from cust_main $where";
 
 my $sql_query = {
   'table'     => 'cust_main',
+  'addl_from' => FS::UI::Web::join_cust_main('cust_main'),
   'hashref'   => {},
   'select'    => join(',',
                    #'cust_main.*',
-                   'custnum',
+                   'cust_main.custnum',
                    $range_cols,
                    $packages_cols,
                    FS::UI::Web::cust_sql_fields(),
index 1dcc37a..bf30477 100644 (file)
@@ -120,6 +120,7 @@ my $fixed = $conf->config("batch-fixed_format-$payby");
 
 tie my %download_formats, 'Tie::IxHash', (
 '' => 'Default batch mode',
+'NACHA' => '94 byte NACHA',
 'csv-td_canada_trust-merchant_pc_batch' => 
               'CSV file for TD Canada Trust Merchant PC Batch',
 'csv-chase_canada-E-xactBatch' =>
index eeef0c0..7b2a170 100755 (executable)
@@ -51,6 +51,7 @@ Examples:
                 'sort_fields'    => \@sort_fields,
                 'align'          => $align,
                 'links'          => \@links,
+                'link_onclicks'  => \@link_onclicks,
                 'color'          => \@color,
                 'style'          => \@style,
 &>
@@ -134,11 +135,12 @@ if ( $cgi->param('tax_names') ) {
   }
 }
 
-my @header = ();
-my @fields = ();
-my @sort_fields = ();
+my @header;
+my @fields;
+my @sort_fields;
 my $align = '';
-my @links = ();
+my @links;
+my @link_onclicks;
 if ( $opt{'pre_header'} ) {
   push @header, @{ $opt{'pre_header'} };
   $align .= 'c' x scalar(@{ $opt{'pre_header'} });
@@ -147,6 +149,16 @@ if ( $opt{'pre_header'} ) {
   push @sort_fields, @{ $opt{'pre_fields'} };
 }
 
+my $sub_receipt = sub {
+  my $obj = shift;
+  my $objnum = $obj->primary_key . '=' . $obj->get($obj->primary_key);
+
+  include('/elements/popup_link_onclick.html',
+    'action'  => $p.'view/cust_pay.html?link=popup;'.$objnum,
+    'actionlabel' => emt('Payment Receipt'),
+  );
+};
+
 push @header, "\u$name_singular",
               'Amount',
 ;
@@ -155,6 +167,7 @@ push @links, '', '';
 push @fields, 'payby_payinfo_pretty',
               sub { sprintf('$%.2f', shift->$amount_field() ) },
 ;
+push @link_onclicks, $sub_receipt, '',
 push @sort_fields, '', $amount_field;
 
 if ( $unapplied ) {
@@ -239,9 +252,12 @@ if ( $cgi->param('magic') ) {
       $title = $part_referral->referral. " $title";
     }
 
-    if ( $cgi->param('cust_classnum') ) {
-      my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
-      push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+    # cust_classnum (false laziness w/ elements/cust_main_dayranges.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+    if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+      my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+      push @search, 'COALESCE( cust_main.classnum, 0) IN ( '.
+                        join(',', map { $_ || '0' } @classnums ).
+                    ' )'
         if @classnums;
     }
 
@@ -250,78 +266,121 @@ if ( $cgi->param('magic') ) {
     }
 
     if ( $cgi->param('payby') ) {
-      $cgi->param('payby') =~
-        /^(CARD|CHEK|BILL|PREP|CASH|WEST|MCRD)(-(VisaMC|Amex|Discover|Maestro))?$/
-          or die "illegal payby ". $cgi->param('payby');
-      push @search, "$table.payby = '$1'";
-      if ( $3 ) {
-
-        my $cardtype = $3;
-
-        my $search;
-        if ( $cardtype eq 'VisaMC' ) {
-          #avoid posix regexes for portability
-          $search =
-            " ( (     substring($table.payinfo from 1 for 1) = '4'     ".
-            "     AND substring($table.payinfo from 1 for 4) != '4936' ".
-            "     AND substring($table.payinfo from 1 for 6)           ".
-            "         NOT SIMILAR TO '49030[2-9]'                        ".
-            "     AND substring($table.payinfo from 1 for 6)           ".
-            "         NOT SIMILAR TO '49033[5-9]'                        ".
-            "     AND substring($table.payinfo from 1 for 6)           ".
-            "         NOT SIMILAR TO '49110[1-2]'                        ".
-            "     AND substring($table.payinfo from 1 for 6)           ".
-            "         NOT SIMILAR TO '49117[4-9]'                        ".
-            "     AND substring($table.payinfo from 1 for 6)           ".
-            "         NOT SIMILAR TO '49118[1-2]'                        ".
-            "   )".
-            "   OR substring($table.payinfo from 1 for 2) = '51' ".
-            "   OR substring($table.payinfo from 1 for 2) = '52' ".
-            "   OR substring($table.payinfo from 1 for 2) = '53' ".
-            "   OR substring($table.payinfo from 1 for 2) = '54' ".
-            "   OR substring($table.payinfo from 1 for 2) = '54' ".
-            "   OR substring($table.payinfo from 1 for 2) = '55' ".
-            "   OR substring($table.payinfo from 1 for 2) = '36' ". #Diner's int'l processed as Visa/MC inside US
-            " ) ";
-        } elsif ( $cardtype eq 'Amex' ) {
-          $search =
-            " (    substring($table.payinfo from 1 for 2 ) = '34' ".
-            "   OR substring($table.payinfo from 1 for 2 ) = '37' ".
-            " ) ";
-        } elsif ( $cardtype eq 'Discover' ) {
-          $search =
-            " (    substring($table.payinfo from 1 for 4 ) = '6011'  ".
-            "   OR substring($table.payinfo from 1 for 2 ) = '65'    ".
-            "   OR substring($table.payinfo from 1 for 3 ) = '622'   ". #China Union Pay processed as Discover outside CN
-            " ) ";
-        } elsif ( $cardtype eq 'Maestro' ) { 
-          $search =
-            " (    substring($table.payinfo from 1 for 2 ) = '63'     ".
-            "   OR substring($table.payinfo from 1 for 2 ) = '67'     ".
-            "   OR substring($table.payinfo from 1 for 6 ) = '564182' ".
-            "   OR substring($table.payinfo from 1 for 4 ) = '4936'   ".
-            "   OR substring($table.payinfo from 1 for 6 )            ".
-            "      SIMILAR TO '49030[2-9]'                             ".
-            "   OR substring($table.payinfo from 1 for 6 )            ".
-            "      SIMILAR TO '49033[5-9]'                             ".
-            "   OR substring($table.payinfo from 1 for 6 )            ".
-            "      SIMILAR TO '49110[1-2]'                             ".
-            "   OR substring($table.payinfo from 1 for 6 )            ".
-            "      SIMILAR TO '49117[4-9]'                             ".
-            "   OR substring($table.payinfo from 1 for 6 )            ".
-            "      SIMILAR TO '49118[1-2]'                             ".
-            " ) ";
-        } else {
-          die "unknown card type $cardtype";
-        }
 
-        my $masksearch = $search;
-        $masksearch =~ s/$table\.payinfo/$table.paymask/gi;
+      my @all_payby_search = ();
+      foreach my $payby ( $cgi->param('payby') ) {
+
+        $payby =~
+          /^(CARD|CHEK|BILL|PREP|CASH|WEST|MCRD)(-(VisaMC|Amex|Discover|Maestro))?$/
+            or die "illegal payby $payby";
+
+        my $payby_search = "$table.payby = '$1'";
+
+        if ( $3 ) {
+
+          my $cardtype = $3;
+
+          my $search;
+          if ( $cardtype eq 'VisaMC' ) {
+            #avoid posix regexes for portability
+            $search =
+              " ( (     substring($table.payinfo from 1 for 1) = '4'     ".
+              "     AND substring($table.payinfo from 1 for 4) != '4936' ".
+              "     AND substring($table.payinfo from 1 for 6)           ".
+              "         NOT SIMILAR TO '49030[2-9]'                        ".
+              "     AND substring($table.payinfo from 1 for 6)           ".
+              "         NOT SIMILAR TO '49033[5-9]'                        ".
+              "     AND substring($table.payinfo from 1 for 6)           ".
+              "         NOT SIMILAR TO '49110[1-2]'                        ".
+              "     AND substring($table.payinfo from 1 for 6)           ".
+              "         NOT SIMILAR TO '49117[4-9]'                        ".
+              "     AND substring($table.payinfo from 1 for 6)           ".
+              "         NOT SIMILAR TO '49118[1-2]'                        ".
+              "   )".
+              "   OR substring($table.payinfo from 1 for 2) = '51' ".
+              "   OR substring($table.payinfo from 1 for 2) = '52' ".
+              "   OR substring($table.payinfo from 1 for 2) = '53' ".
+              "   OR substring($table.payinfo from 1 for 2) = '54' ".
+              "   OR substring($table.payinfo from 1 for 2) = '54' ".
+              "   OR substring($table.payinfo from 1 for 2) = '55' ".
+#              "   OR substring($table.payinfo from 1 for 2) = '36' ". #Diner's int'l was processed as Visa/MC inside US, now Discover
+              " ) ";
+          } elsif ( $cardtype eq 'Amex' ) {
+            $search =
+              " (    substring($table.payinfo from 1 for 2 ) = '34' ".
+              "   OR substring($table.payinfo from 1 for 2 ) = '37' ".
+              " ) ";
+          } elsif ( $cardtype eq 'Discover' ) {
+
+            my $conf = new FS::Conf;
+            my $country = $conf->config('countrydefault') || 'US';
+
+            $search =
+              " (    substring($table.payinfo from 1 for 4 ) = '6011'  ".
+              "   OR substring($table.payinfo from 1 for 2 ) = '65'    ".
+              "   OR substring($table.payinfo from 1 for 3 ) = '300'   ".
+              "   OR substring($table.payinfo from 1 for 3 ) = '301'   ".
+              "   OR substring($table.payinfo from 1 for 3 ) = '302'   ".
+              "   OR substring($table.payinfo from 1 for 3 ) = '303'   ".
+              "   OR substring($table.payinfo from 1 for 3 ) = '304'   ".
+              "   OR substring($table.payinfo from 1 for 3 ) = '305'   ".
+              "   OR substring($table.payinfo from 1 for 4 ) = '3095'  ".
+              "   OR substring($table.payinfo from 1 for 2 ) = '36'    ".
+              "   OR substring($table.payinfo from 1 for 2 ) = '38'    ".
+              "   OR substring($table.payinfo from 1 for 2 ) = '39'    ".
+              "   OR substring($table.payinfo from 1 for 3 ) = '644'   ".
+              "   OR substring($table.payinfo from 1 for 3 ) = '645'   ".
+              "   OR substring($table.payinfo from 1 for 3 ) = '646'   ".
+              "   OR substring($table.payinfo from 1 for 3 ) = '647'   ".
+              "   OR substring($table.payinfo from 1 for 3 ) = '648'   ".
+              "   OR substring($table.payinfo from 1 for 3 ) = '649'   ".
+              ( $country =~ /^(US|CA)$/
+               ?" OR substring($table.payinfo from 1 for 4 ) = '3528'  ". # JCB cards in the 3528-3589 range identified as Discover inside US/CA
+                " OR substring($table.payinfo from 1 for 4 ) = '3529'  ".
+                " OR substring($table.payinfo from 1 for 3 ) = '353'   ".
+                " OR substring($table.payinfo from 1 for 3 ) = '354'   ".
+                " OR substring($table.payinfo from 1 for 3 ) = '355'   ".
+                " OR substring($table.payinfo from 1 for 3 ) = '356'   ".
+                " OR substring($table.payinfo from 1 for 3 ) = '357'   ".
+                " OR substring($table.payinfo from 1 for 3 ) = '358'   "
+               :""
+              ).
+              "   OR substring($table.payinfo from 1 for 3 ) = '622'   ". #China Union Pay processed as Discover outside CN
+              " ) ";
+          } elsif ( $cardtype eq 'Maestro' ) { 
+            $search =
+              " (    substring($table.payinfo from 1 for 2 ) = '63'     ".
+              "   OR substring($table.payinfo from 1 for 2 ) = '67'     ".
+              "   OR substring($table.payinfo from 1 for 6 ) = '564182' ".
+              "   OR substring($table.payinfo from 1 for 4 ) = '4936'   ".
+              "   OR substring($table.payinfo from 1 for 6 )            ".
+              "      SIMILAR TO '49030[2-9]'                             ".
+              "   OR substring($table.payinfo from 1 for 6 )            ".
+              "      SIMILAR TO '49033[5-9]'                             ".
+              "   OR substring($table.payinfo from 1 for 6 )            ".
+              "      SIMILAR TO '49110[1-2]'                             ".
+              "   OR substring($table.payinfo from 1 for 6 )            ".
+              "      SIMILAR TO '49117[4-9]'                             ".
+              "   OR substring($table.payinfo from 1 for 6 )            ".
+              "      SIMILAR TO '49118[1-2]'                             ".
+              " ) ";
+          } else {
+            die "unknown card type $cardtype";
+          }
+
+          my $masksearch = $search;
+          $masksearch =~ s/$table\.payinfo/$table.paymask/gi;
+
+          $payby_search = "( $payby_search AND ( $search OR ( $table.paymask IS NOT NULL AND $masksearch ) ) )";
 
-        push @search,
-          "( $search OR ( $table.paymask IS NOT NULL AND $masksearch ) )";
+        }
+
+        push @all_payby_search, $payby_search;
 
       }
+
+      push @search, ' ( '. join(' OR ', @all_payby_search). ' ) ' if @all_payby_search;
+
     }
 
     if ( $cgi->param('payinfo') ) {
@@ -350,6 +409,7 @@ if ( $cgi->param('magic') ) {
     }
 
     my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
+
     push @search, "_date >= $beginning ",
                   "_date <= $ending";
 
@@ -411,7 +471,7 @@ if ( $cgi->param('magic') ) {
   #here is the agent virtualization
   push @search, $curuser->agentnums_sql;
 
-  my $addl_from = ' LEFT JOIN cust_main USING ( custnum ) ';
+  my $addl_from = FS::UI::Web::join_cust_main($table);
   my $group_by = '';
 
   if ( $cgi->param('tax_names') ) {
index 0e04ab0..0462f1c 100644 (file)
@@ -30,68 +30,33 @@ Examples:
   <TR>
     <TD ALIGN="right"><% ucfirst(PL($name_singular)) %> of type: </TD>
     <TD>
-      <SELECT NAME="payby" onChange="payby_changed(this)">
-        <OPTION VALUE=""><% mt('all') |h %></OPTION>
-        <OPTION VALUE="CARD"><% mt('credit card (all)') |h %></OPTION>
-        <OPTION VALUE="CARD-VisaMC"><% mt('credit card (Visa/MasterCard)') |h %></OPTION>
-        <OPTION VALUE="CARD-Amex"><% mt('credit card (American Express)') |h %></OPTION>
-        <OPTION VALUE="CARD-Discover"><% mt('credit card (Discover)') |h %></OPTION>
-        <OPTION VALUE="CARD-Maestro"><% mt('credit card (Maestro/Switch/Solo)') |h %></OPTION>
-        <OPTION VALUE="CHEK"><% mt('electronic check / ACH') |h %></OPTION>
-        <OPTION VALUE="BILL"><% mt('check') |h %></OPTION>
-        <OPTION VALUE="PREP"><% mt('prepaid card') |h %></OPTION>
-        <OPTION VALUE="CASH"><% mt('cash') |h %></OPTION>
-        <OPTION VALUE="WEST"><% mt('Western Union') |h %></OPTION>
-        <OPTION VALUE="MCRD"><% mt('manual credit card') |h %></OPTION>
+      <SELECT NAME="payby" SIZE=10 MULTIPLE>
+%#        <OPTION VALUE=""><% mt('all') |h %></OPTION>
+%#        <OPTION VALUE="CARD"><% mt('credit card (all)') |h %></OPTION>
+        <OPTION VALUE="CARD-VisaMC" SELECTED><% mt('credit card (Visa/MasterCard)') |h %></OPTION>
+        <OPTION VALUE="CARD-Amex" SELECTED><% mt('credit card (American Express)') |h %></OPTION>
+        <OPTION VALUE="CARD-Discover" SELECTED><% mt('credit card (Discover)') |h %></OPTION>
+        <OPTION VALUE="CARD-Maestro" SELECTED><% mt('credit card (Maestro/Switch/Solo)') |h %></OPTION>
+        <OPTION VALUE="CHEK" SELECTED><% mt('electronic check / ACH') |h %></OPTION>
+        <OPTION VALUE="BILL" SELECTED><% mt('check') |h %></OPTION>
+        <OPTION VALUE="PREP" SELECTED><% mt('prepaid card') |h %></OPTION>
+        <OPTION VALUE="CASH" SELECTED><% mt('cash') |h %></OPTION>
+        <OPTION VALUE="WEST" SELECTED><% mt('Western Union') |h %></OPTION>
+        <OPTION VALUE="MCRD" SELECTED><% mt('manual credit card') |h %></OPTION>
       </SELECT>
     </TD>
   </TR>
 
-  <SCRIPT TYPE="text/javascript">
-  
-    function payby_changed(what) {
-      if ( what.value == 'BILL' ) {
-        show('payinfo');
-        hide('ccpay');
-      } else if ( what.value.match(/^CARD|CHEK/) ) {
-        hide('payinfo');
-        show('ccpay');
-      } else {
-        hide('payinfo');
-        hide('ccpay');
-      }
-    }
-
-    function show(what) {
-      document.getElementById(what+'_caption').style.color = '#000000';
-      document.getElementById(what).disabled = false;
-      document.getElementById(what).style.backgroundColor = '#ffffff';
-    }
-
-    function hide(what) {
-      document.getElementById(what+'_caption').style.color = '#bbbbbb';
-      document.getElementById(what).disabled = true;
-      document.getElementById(what).style.backgroundColor = '#dddddd';
-    }
-
-
-
-  </SCRIPT>
-
   <TR>
-    <TD ALIGN="right"><FONT ID="payinfo_caption" COLOR="#bbbbbb"><% mt('Check #:') |h %> </FONT></TD>
+    <TD ALIGN="right"><% mt('Check #:') |h %> </TD>
     <TD>
-      <INPUT TYPE="text" ID="payinfo" NAME="payinfo" DISABLED STYLE="background-color: #dddddd">
+      <INPUT TYPE="text" ID="payinfo" NAME="payinfo">
     </TD>
   </TR>
   <TR>
-    <TD ALIGN="right">
-      <FONT ID="ccpay_caption" COLOR="#bbbbbb">
-        <% mt('Transaction #') |h %>
-      </FONT>
-    </TD>
+    <TD ALIGN="right"><% mt('Transaction #:') |h %> </TD>
     <TD>
-      <INPUT TYPE="text" ID="ccpay" NAME="ccpay" DISABLED STYLE="background-color: #dddddd">
+      <INPUT TYPE="text" ID="ccpay" NAME="ccpay">
     </TD>
   </TR>
 
@@ -108,7 +73,8 @@ Examples:
     <TD>
       <TABLE>
         <& /elements/tr-input-beginning_ending.html,
-                      layout   => 'horiz',
+                      layout     => 'horiz',
+                      input_time => $conf->exists('report-cust_pay-select_time'),
         &>
       </TABLE>
     </TD>
@@ -158,6 +124,8 @@ my $name_singular = $opt{'name_singular'};
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
 
+my $conf = new FS::Conf;
+
 my $void = $cgi->param('void') ? 1 : 0;
 my $unapplied = $cgi->param('unapplied') ? 1 : 0;
 
diff --git a/httemplate/search/elements/report_svc_Common.html b/httemplate/search/elements/report_svc_Common.html
new file mode 100644 (file)
index 0000000..4341970
--- /dev/null
@@ -0,0 +1,122 @@
+<%doc>
+
+Example:
+
+  <& elements/report_svc_Common.html,
+
+    #required
+    'table' => 'svc_something',
+    'title'  => 'Page title',
+
+    #optional
+    'action' => 'svc_tablename.html', #defaults to svc_tablename.html
+
+  &>
+
+</%doc>
+<& /elements/header.html, $title &>
+
+<FORM ACTION="<% $opt{'action'} || $opt{'table'}. '.html' %>" METHOD="GET">
+<INPUT TYPE="hidden" NAME="magic" VALUE="advanced">
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
+
+  <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
+
+    <TR>
+      <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1"><% mt('Search options') |h %></FONT></TH>
+    </TR>
+
+% unless ( $custnum ) {
+
+    <& /elements/tr-select-agent.html,
+         curr_value    => scalar( $cgi->param('agentnum') ),
+         disable_empty => 0,
+    &>
+
+    <& /elements/tr-select-cust_main-status.html,
+         label         => 'Customer Status',
+         field         => 'cust_status',
+    &>
+
+    <& /elements/tr-select-payby.html,
+         label         => emt('Payment method:'),
+         payby_type    => 'cust',
+         multiple      => 1,
+         all_selected  => 1,
+    &>
+
+    <& /elements/tr-input-money.html,
+         label         => 'Balance over',
+         field         => 'balance',
+    &>
+
+    <& /elements/tr-input-text.html,
+         label         => 'Balance age (days)',
+         field         => 'balance_days',
+         size          => 4,
+    &>
+
+% }
+
+%   # just this customer's domains?
+%#    <& /elements/tr-select-domain.html,
+%#                   'element_name'  => 'domsvc',
+%#                   'curr_value'    => scalar( $cgi->param('domsvc') ),
+%#                   'disable_empty' => 0,
+%#    &>
+
+    <& /elements/tr-selectmultiple-part_pkg.html &> 
+
+    <& /elements/tr-select-part_svc.html,
+         'svcdb' => $svcdb,
+         'label' => 'Services',
+    &> 
+
+    <TR>
+      <TH CLASS="background" COLSPAN=2>&nbsp;</TH>
+    </TR>
+    <TR>
+      <TH CLASS="background" COLSPAN=2 ALIGN="left"><FONT SIZE="+1"><% mt('Display options') |h %></FONT></TH>
+    </TR>
+
+%   #"package fields" ala advanced svc_acct search?
+%   #move to /elements/tr-select-cust_pkg-fields and use it from there if so...
+
+    <& /elements/tr-select-cust-fields.html &>
+                       
+  </TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="<% mt('Get Report') |h %>">
+
+</FORM>
+
+<& /elements/footer.html &>
+<%init>
+
+my(%opt) = @_;
+
+my $svcdb = $opt{'table'};
+
+my $name =        "FS::$svcdb"->table_info->{'name_plural'}
+           || PL( "FS::$svcdb"->table_info->{'name'}        );
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right("Services: $name: Advanced search");
+
+my $title = $opt{'title'};
+
+#false laziness w/report_cust_pkg.html
+my( $custnum, $cust_main) = ('', '');
+if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+  $custnum = $1;
+  my $cust_main = qsearchs({
+    'table'     => 'cust_main', 
+    'hashref'   => { 'custnum' => $custnum },
+    'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+  }) or die "unknown custnum $custnum";
+  $title = mt("$title: [_1]", $cust_main->name);
+}
+
+</%init>
index 7ccf356..e760bc5 100644 (file)
 %                   $bgcolor = $bgcolor1;
 %                 }
 
-                  <TR>
+%                 my $trid = '';
+%                   if ( $opt{'link_field' } ) {
+%                     my $link_field = $opt{'link_field'};
+%                     if ( ref($link_field) eq 'CODE' ) {
+%                       $trid = &{$link_field}($row);
+%                     } else {
+%                       $trid = $row->$link_field();
+%                     }
+%                   }
+                  <TR ID="<%$trid |h%>">
+                      
 
 %                   if ( $opt{'fields'} ) {
 %
index 5a16a22..d44b454 100644 (file)
@@ -167,6 +167,11 @@ Example:
     # miscellany
    'download_label' => 'Download this report',
                         # defaults to 'Download full results' 
+   'link_field'     => 'pkgpart'
+                        # will create internal links for each row,
+                        # with the value of this field as the NAME attribute
+                        # If this is a coderef, will evaluate it, passing the
+                        # row as an argument, and use the result as the NAME.
   &>
 
 </%doc>
@@ -348,7 +353,7 @@ if ( $opt{'disableable'} ) {
 my $limit = '';
 my($confmax, $maxrecords, $offset );
 
-unless ( $type =~ /^(csv|\w*.xls)$/) {
+unless ( $type =~ /^(csv|xml|\w*.xls)$/) {
 # html mode
   unless (exists($opt{count_query}) && length($opt{count_query})) {
     ( $opt{count_query} = $opt{query} ) =~
diff --git a/httemplate/search/elements/svc_Common.html b/httemplate/search/elements/svc_Common.html
new file mode 100644 (file)
index 0000000..56c75bb
--- /dev/null
@@ -0,0 +1,48 @@
+<& search.html, %opt &>
+<%doc>
+Currently does nothing but insert the classnames for fields chosen from an
+inventory class.
+</%doc>
+<%init>
+my %opt = @_;
+my $query = $opt{query};
+my $svcdb = $query->{'table'};
+
+# to avoid looking up the inventory class of every service in the database,
+# keep as much of the base query as possible.
+my $item_query = { %$query };
+$item_query->{'table'} = 'inventory_item';
+$item_query->{'addl_from'} = 
+  " JOIN ( $svcdb ". $query->{'addl_from'} .
+  ") ON inventory_item.svcnum = $svcdb.svcnum ".
+  " JOIN inventory_class ON (inventory_item.classnum = inventory_class.classnum)";
+# avoid conflict with inventory_item.agentnum
+$item_query->{'extra_sql'} =~ s/ agentnum/ cust_main.agentnum/g;
+$item_query->{'select'} = 'inventory_item.svcnum, '.
+                          'inventory_item.svc_field, '.
+                          'inventory_class.classname';
+my @items = qsearch($item_query);
+my %item_fields;
+foreach my $i (@items) {
+  $item_fields{ $i->svc_field } ||= {};
+  $item_fields{ $i->svc_field }{ $i->svcnum } = $i->classname;
+}
+
+$opt{'sort_fields'} ||= [];
+for ( my $i = 0; $i < @{ $opt{'fields'} }; $i++ ) {
+  my $f = $opt{'fields'}[$i];
+  next if ref($f); # it's not a plain table column
+  $opt{'sort_fields'}[$i] ||= $f;
+  my $classnames = $item_fields{$f}; # hashref of svcnum -> classname
+  next if !$classnames; # there are no inventory items in this column
+  $opt{'fields'}[$i] = sub {
+    my $svc = $_[0];
+    if ( exists($classnames->{$svc->svcnum}) ) {
+      return $svc->$f . '<BR><I>('. $classnames->{$svc->svcnum} . ')</I>';
+    } else {
+      return $svc->$f;
+    }
+  }; #sub
+}
+
+</%init>
index 753c7bf..2bc6ff4 100644 (file)
@@ -7,7 +7,7 @@
 <%init>
 
 die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+  unless $FS::CurrentUser::CurrentUser->access_right('Employees: Audit Report');
 
 my %tables = (
     cust_pay        => 'Payments',
index 086c8e9..0e4251f 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'       => $title,
 
                  'menubar'     => [ 'View inventory classes' =>
@@ -87,8 +87,8 @@
 <INPUT TYPE="hidden" NAME="classnum" VALUE="$classnum">
 <INPUT TYPE="hidden" NAME="avail"    VALUE="! .$cgi->param('avail') . '">', #'
                   'html_foot' => $sub_foot,
-             )
-%>
+             
+&>
 <%init>
 
 my $curuser = $FS::CurrentUser::CurrentUser;
@@ -157,7 +157,7 @@ my $link_cust = sub {
 my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
                 ' LEFT JOIN part_svc  USING ( svcpart ) '.
                 ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
-                ' LEFT JOIN cust_main USING ( custnum ) ';
+                FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
 my $areboxes = 0;
 
 my $sub_checkbox = sub {
index ee395f4..a678d45 100644 (file)
@@ -1,4 +1,4 @@
-<% include('elements/search.html',
+<& elements/search.html,
              'title'         => $title,
              'name_singular' => 'member',
              'query'         => $query,
@@ -6,8 +6,7 @@
              'header'        => [ 'Email address' ],
              'fields'        => [ $email_sub, ], #just this one for now
              'html_init'     => $html_init,
-          )
-%>
+&>
 <%init>
 
 #XXX ACL:
index 57da9d4..a90f13c 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
               'title'        => $title,
               'name_singular' => $name,
               'header'       => \@header,
@@ -14,8 +14,8 @@
               'links'        => \@links,
               'align'        => $align,
               'sort_fields'  => [],
-          )
-%>
+          
+&>
 <%init>
 
 #this is about reports about packages definitions (starting w/commission ones)
@@ -23,7 +23,7 @@
 
 my $curuser = $FS::CurrentUser::CurrentUser;
 die "access denied"
-  unless $curuser->access_right('Financial reports');
+  unless $curuser->access_right('Employees: Commission Report'); #that's all this does so far
 
 my $conf = new FS::Conf;
 my $money_char = $conf->config('money_char') || '$';
index aeaa012..620996a 100755 (executable)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'         => 'Payment Batches',
                 'name_singular' => 'batch',
                 'query'         => { 'table'     => 'pay_batch',
                                    ],
                  'html_init'     => $html_init,
                  'html_foot'     => include('.upload_incoming'),
-      )
-%>
+&>
 <%def .upload_incoming>
 % if ( FS::payment_gateway->count("gateway_namespace = 'Business::BatchPayment' AND disabled IS NULL") > 0 ) { 
 <& /elements/form-file_upload.html,
@@ -149,16 +148,10 @@ my $count_query = 'SELECT COUNT(*) FROM pay_batch';
 my($begin, $end) = ( '', '' );
 
 my @where;
-if ( $cgi->param('beginning')
-     && $cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/ ) {
-  $begin = parse_datetime($1);
-  push @where, "download >= $begin";
-}
-if ( $cgi->param('ending')
-      && $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/ ) {
-  $end = parse_datetime($1) + 86399;
-  push @where, "download < $end";
-}
+
+my($beginning,$ending) = FS::UI::Web::parse_beginning_ending($cgi);
+push @where, "( (download >= $beginning AND download <= $ending) ".
+             ' OR download IS NULL )';
 
 my @status;
 if ( $cgi->param('open') ) {
index 1335379..faf3544 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
               'title'         => 'Phone Number (DID) Search Results',
               'name_singular' => 'phone number',
               'query'         => {
@@ -81,8 +81,8 @@
                            FS::UI::Web::cust_styles(),
                           '',
                          ],
-      )
-%>
+      
+&>
 <%init>
 
 die "access denied"
@@ -125,9 +125,11 @@ my $search = scalar(@search)
 my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
                 #' LEFT JOIN part_svc  USING ( svcpart ) '.
                 ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
-                ' LEFT JOIN cust_main USING ( custnum ) ';
+                FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
 
 my $count_query = "SELECT COUNT(*) FROM phone_avail $search"; #$addl_from?
+# All of these relationships are left joined in the many-to-one direction,
+# so including $addl_from won't affect the count.  Logic!
 
 my $hashref = {};
 $hashref->{'ordernum'} = $1 if $cgi->param('ordernum') =~ /^(\d+)$/;
index 03d2154..b3efdbd 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
               'title'         => 'LATA Search Results',
               'name_singular' => 'LATA',
               'query'         => {
@@ -72,8 +72,8 @@
                            '',
                            '',
                          ],
-      )
-%>
+      
+&>
 <%init>
 
 die "access denied"
index 03d121d..cb58a66 100644 (file)
@@ -129,10 +129,13 @@ if ( $cgi->param('status') =~ /^([a-z]+)$/ ) {
   push @where, FS::cust_main->cust_status_sql . " = '$status'";
 }
 
-if ( $cgi->param('cust_classnum') ) {
-  my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+  my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
   $link .= ";cust_classnum=$_" foreach @classnums;
-  push @where, 'cust_main.classnum IN('.join(',',@classnums).')'
+  push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+                   join(',', map { $_ || '0' } @classnums ).
+               ' )'
     if @classnums;
 }
 
index 3640351..7566e65 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'       => 'Unused Prepaid Cards'.
                                   ($agent ? ' for '. $agent->agent : ''),
                  'menubar'     => [
@@ -47,8 +47,8 @@
                          $agent ? [ "${p}edit/agent.cgi?", 'agentnum' ] : '';
                        },
                  ],
-      )
-%>
+      
+&>
 <%init>
 
 die "access denied"
index 328d120..ab37b90 100644 (file)
@@ -1,4 +1,4 @@
-<% include('elements/search.html',
+<& elements/search.html,
              'title'         => 'Prospect Search Results',
              'name_singular' => 'prospect',
              'query'         => $query,
@@ -23,8 +23,7 @@
                                   '', #link to contact edit???
                                 ],
              'agent_virt'    => 1,
-          )
-%>
+&>
 <%init>
 
 die "access denied"
index 7133ef0..7b718e4 100755 (executable)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'         => 'Qualifications',
                 'name_singular' => 'qualification',
                 'query'         => { 'table'     => 'qual',
@@ -51,8 +51,8 @@
                                      '',
                                      '',
                                    ],
-      )
-%>
+      
+&>
 <%init>
 
 die "access denied"
index 1c12470..141c535 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'       => 'Job Queue',
                  'name'        => 'jobs',
                 'html_form'   => qq!<FORM NAME="jobForm" ACTION="$p/misc/queue.cgi" METHOD="POST">!,
                                     '';
                                   }
                                 },
-             )
-
-%>
+             
+&>
 <%init>
 
 die "access denied"
index 259c85c..fbc35be 100755 (executable)
@@ -72,7 +72,7 @@ die "access denied"
   unless $curuser->access_right('List quotations');
 
 my $join_prospect_main = 'LEFT JOIN prospect_main USING ( prospectnum )';
-my $join_cust_main = 'LEFT JOIN cust_main ON ( quotation.custnum = cust_main.custnum )';
+my $join_cust_main = FS::UI::Web::join_cust_main('quotation');
 
 #here is the agent virtualization
 my $agentnums_sql = ' (    '. $curuser->agentnums_sql( table=>'prospect_main' ).
index f7d6d20..42211e5 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'       => 'Unused Registration Codes for '.
                                   $agent->agent,
                  'name'        => 'registration codes',
@@ -23,8 +23,8 @@
                    #$plink,
                    '',
                  ],
-      )
-%>
+      
+&>
 <%init>
 
 die "access denied"
index f593a94..b842b1f 100755 (executable)
                      'table'        => 'part_pkg_report_option',
                      'name_col'     => 'name',
                      'hashref'      => { 'disabled' => '' },
-                     'element_name' => 'partv_report_option',
+                     'element_name' => 'part5_report_option',
+                     'curr_value'   =>
+                            FS::Report::FCC_477::restore_fcc477map("part5_report_option"),
                  )
             %>
         </TD>
diff --git a/httemplate/search/report_agent_commission.html b/httemplate/search/report_agent_commission.html
new file mode 100644 (file)
index 0000000..79f94c5
--- /dev/null
@@ -0,0 +1,22 @@
+<% include('/elements/header.html', 'Agent commission report' ) %>
+
+<FORM ACTION="agent_commission.html">
+
+<TABLE BGCOLOR="#cccccc" CELLSPACING=0>
+
+<% include( '/elements/tr-select-agent.html', disable_empty => 1 ) %>
+
+<% include( '/elements/tr-input-beginning_ending.html', ) %>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Get Report">
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+
+</%init>
index 51618fb..b339c80 100644 (file)
@@ -4,7 +4,7 @@
 <INPUT TYPE="hidden" NAME="magic" VALUE="_date">
 <INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
 
-<TABLE BGCOLOR="#cccccc" CELLSPACING=0
+<TABLE BGCOLOR="#cccccc" CELLSPACING=0>
 
 % unless ( $custnum ) {
   <& /elements/tr-select-agent.html,
index acc49ae..bac4346 100755 (executable)
       </TR>
 %   }
 
-    <& /elements/tr-select-cust_tag.html,
-                  'cgi'                 => $cgi,
-                  'is_report'    => 1,
-                  'multiple'     => 1,
-    &>
+      <TR>
+        <TD ALIGN="right">Tags</TD>
+        <TD>
+            <& /elements/select-cust_tag.html,
+                          'cgi'                => $cgi,
+                          'is_report'   => 1,
+                          'multiple'    => 1,
+            &>
+          <DIV STYLE="display:inline-block; vertical-align:baseline">
+            <INPUT TYPE="radio" NAME="all_tags" VALUE="0" CHECKED> Any of these
+            <BR>
+            <INPUT TYPE="radio" NAME="all_tags" VALUE="1"> All of these
+          </DIV>
+        </TD>
+      </TR>
 
     <& /elements/tr-select-payby.html,
                   'payby_type'   => 'cust',
     </TR>
 
     <TR>
+      <TD ALIGN="right" VALIGN="center"><% mt('With postal mail invoices') |h %></TD>
+        <TD><INPUT TYPE="checkbox" NAME="POST" ID="POST" onClick="POST_changed();"></TD>
+    </TR>
+
+    <TR>
       <TD ALIGN="right" VALIGN="center"><% mt('Without postal mail invoices') |h %></TD>
-        <TD><INPUT TYPE="checkbox" NAME="no_POST"></TD>
+        <TD><INPUT TYPE="checkbox" NAME="no_POST" ID="no_POST" onClick="no_POST_changed();"></TD>
     </TR>
 
+    <SCRIPT TYPE="text/javascript">
+      function POST_changed() {
+        if ( document.getElementById('POST').checked == true ) {
+          document.getElementById('no_POST').checked = false;
+        }
+      }
+      function no_POST_changed() {
+        if ( document.getElementById('no_POST').checked == true ) {
+          document.getElementById('POST').checked = false;
+        }
+      }
+    </SCRIPT>
+
     <TR>
       <TH CLASS="background" COLSPAN=2>&nbsp;</TH>
     </TR>
index 757b823..461849b 100644 (file)
@@ -23,7 +23,7 @@
 <%init>
 
 die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+  unless $FS::CurrentUser::CurrentUser->access_right('Employees: Audit Report');
 
 my %tables = (
     cust_pay        => 'Payments',
index 51afad3..ebfcae8 100644 (file)
@@ -25,6 +25,6 @@
 <%init>
 
 die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+  unless $FS::CurrentUser::CurrentUser->access_right('Employees: Commission Report');
 
 </%init>
index 5cff0f4..854b24a 100755 (executable)
   <& /elements/tr-select-cust_main-status.html,
                 'label' => emt('Customer Status'),
   &>
-  
+
+  <& /elements/tr-select-cust_class.html,
+     'label'        => emt('Customer class'),
+     'field'        => 'cust_classnum',
+     'multiple'     => 1,
+     'pre_options'  => [ '' => emt('(none)') ],
+     'all_selected' => 1,
+  &>
+
   <TR>
     <TD ALIGN="right"><% mt('Customers') |h %></TD>
     <TD>
index 01215e8..7e54465 100644 (file)
@@ -8,13 +8,18 @@
   'empty_label'   => 'all',
 &>
 
-% my @exporttypes = map { "'$_'" } qw(sqlradius broadband_sqlradius);
+%#more future-proof to actually ask all exports if they ->can('usage_sessions')
+% my @exporttypes = qw( sqlradius sqlradius_withdomain broadband_sqlradius
+%                       phone_sqlradius radiator
+%                     );
 <& /elements/tr-select-table.html,
   'label'         => 'Export',
   'table'         => 'part_export',
   'name_col'      => 'label',
   'hashref'       => {},
-  'extra_sql'     => ' WHERE exporttype IN('.join(',', @exporttypes).')',
+  'extra_sql'     => ' WHERE exporttype IN ( '.
+                                            join(',', map "'$_'", @exporttypes).
+                                          ')',
   'disable_empty' => 1,
   'order_by'      => 'ORDER BY exportnum',
 &>
index 74bf553..e47f727 100755 (executable)
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Services: Accounts: Advanced search'); #?
 
-my $title = emt('Account Report');
+my $title = mt('Account Report');
 
 #false laziness w/report_cust_pkg.html
 my $custnum = '';
@@ -127,7 +127,7 @@ if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
     'hashref'   => { 'custnum' => $custnum },
     'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
   }) or die "unknown custnum $custnum";
-  $title = emt("Account Report: [_1]", $cust_main->name);
+  $title = mt("Account Report: [_1]", $cust_main->name);
 }
 
 </%init>
index 9f10426..63ca03e 100644 (file)
@@ -1,32 +1,6 @@
-<% include('/elements/header.html', 'Phone number total usage' ) %>
+<& elements/report_svc_Common.html,
+     'table'   => 'svc_phone',
+     'title'   => 'Phone number report',
 
-<FORM ACTION="svc_phone.cgi" METHOD="GET">
-
-<INPUT TYPE="hidden" NAME="magic"       VALUE="all">
-<INPUT TYPE="hidden" NAME="usage_total" VALUE="1">
-
-<TABLE BGCOLOR="#cccccc" CELLSPACING=0>
-
-%#  <TR>
-%#    <TH CLASS="background" COLSPAN=2 ALIGN="left">
-%#     <FONT SIZE="+1">Search options</FONT>
-%#    </TH>
-%#  </TR>
-
-  <% include ( '/elements/tr-input-beginning_ending.html', prefix=>'usage' ) %>
-
-</TABLE>
-
-<BR>
-<INPUT TYPE="submit" VALUE="Search phone numbers">
-
-</FORM>
-
-<% include('/elements/footer.html') %>
-<%init>
-
-#? 'List services' ?  something new?
-die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('List rating data');
-
-</%init>
+     'action'  => 'svc_phone.cgi',
+&>
diff --git a/httemplate/search/report_svc_phone_usage.html b/httemplate/search/report_svc_phone_usage.html
new file mode 100644 (file)
index 0000000..9f10426
--- /dev/null
@@ -0,0 +1,32 @@
+<% include('/elements/header.html', 'Phone number total usage' ) %>
+
+<FORM ACTION="svc_phone.cgi" METHOD="GET">
+
+<INPUT TYPE="hidden" NAME="magic"       VALUE="all">
+<INPUT TYPE="hidden" NAME="usage_total" VALUE="1">
+
+<TABLE BGCOLOR="#cccccc" CELLSPACING=0>
+
+%#  <TR>
+%#    <TH CLASS="background" COLSPAN=2 ALIGN="left">
+%#     <FONT SIZE="+1">Search options</FONT>
+%#    </TH>
+%#  </TR>
+
+  <% include ( '/elements/tr-input-beginning_ending.html', prefix=>'usage' ) %>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Search phone numbers">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+#? 'List services' ?  something new?
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('List rating data');
+
+</%init>
index 42a52d1..479b990 100755 (executable)
@@ -250,8 +250,10 @@ my $conf = new FS::Conf;
 my $out = 'Out of taxable region(s)';
 
 my %label_opt = ( out => 1 ); #enable 'Out of Taxable Region' label
-$label_opt{no_city} = 1     unless $cgi->param('show_cities');
-$label_opt{no_taxclass} = 1 unless $cgi->param('show_taxclasses');
+$label_opt{with_city} = 1     if $cgi->param('show_cities');
+$label_opt{with_district} = 1 if $cgi->param('show_districts');
+
+$label_opt{with_taxclass} = 1 if $cgi->param('show_taxclasses');
 
 my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
 
@@ -487,7 +489,8 @@ my $tot_tax = 0;
 my $tot_credit = 0;
 
 my @loc_params = qw(country state county);
-push @loc_params, qw(city district) if $cgi->param('show_cities');
+push @loc_params, 'city' if $cgi->param('show_cities');
+push @loc_params, 'district' if $cgi->param('show_districts');
 
 foreach my $r ( qsearch({ 'table'     => 'cust_main_county', })) {
   my $taxnum = $r->taxnum;
@@ -522,7 +525,7 @@ foreach my $r ( qsearch({ 'table'     => 'cust_main_county', })) {
   }
 
   if ( $cgi->param('show_taxclasses') ) {
-    my $base_label = $r->label(%label_opt, 'no_taxclass' => 1);
+    my $base_label = $r->label(%label_opt, 'with_taxclass' => 0);
     $base_regions{$base_label} ||=
     {
       label   => $base_label,
index 2ab0e0b..8a207aa 100755 (executable)
 
 %    if ( $city ) {
    <TR>
-     <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_cities" VALUE="1"></TD>
+     <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_cities" VALUE="1" onclick="toggle_show_cities(this)"></TD>
      <TD>Show cities</TD>
    </TR>
+   <TR>
+     <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="show_districts" VALUE="1" DISABLED></TD>
+     <TD>Show districts</TD>
+   </TR>
+  <SCRIPT TYPE="text/javascript">
+  function toggle_show_cities() {
+    what = document.getElementsByName('show_cities')[0];
+    what.form.show_districts.disabled = !what.checked;
+    what.form.show_districts.checked  = what.checked;
+  }
+  toggle_show_cities();
+  </SCRIPT>
 % } 
 
 %    if ( $conf->exists('enable_taxclasses') ) {
index 1ed5a38..f5ac023 100644 (file)
@@ -1,21 +1,21 @@
-<% include('elements/search.html',
+<& elements/search.html,
              'title'         => 'Time worked summary',
              'name_singular' => 'ticket',
              'query'         => $query,
              'count_query'   => $count_query,
              'count_addl'    => [ $format_seconds_sub,
-                                  $applied_time ? $format_seconds_sub : () ],
+                                  $applied ? $format_seconds_sub : () ],
              'header'        => [ 'Ticket #',
                                   'Ticket',
                                   'Time',
-                                  $applied_time ? 'Applied' : (),
+                                  $applied ? 'Applied' : (),
                                 ],
              'fields'        => [ 'ticketid',
                                   sub { encode_entities(shift->get('subject')) },
                                   sub { my $seconds = shift->get('ticket_time');
                                         &{ $format_seconds_sub }( $seconds );
                                       },
-                                  ($applied_time ?
+                                  ($applied ?
                                     sub { my $seconds = shift->get('applied_time');
                                         &{ $format_seconds_sub }( $seconds );
                                       } : () ),
@@ -23,7 +23,7 @@
              'sort_fields'   => [ 'ticketid',
                                   'subject',
                                   'transaction_time',
-                                  $applied_time ? 'applied_time' : (),
+                                  $applied ? 'applied_time' : (),
                                 ],
              'links'         => [
                                   $link,
@@ -31,8 +31,7 @@
                                   '',
                                   '',
                                 ],
-          )
-%>
+&>
 <%once>
 
 my $format_seconds_sub = sub {
@@ -60,7 +59,6 @@ my @select = (
 );
 my @select_total = ( 'COUNT(*)' );
 
-my ($transaction_time, $applied_time);
 my $join = 'JOIN Users   ON Transactions.Creator = Users.Id '; #.
 
 my $twhere = "
@@ -68,6 +66,8 @@ my $twhere = "
     AND Transactions.ObjectId = Tickets.Id
 ";
 
+my $transaction_time;
+my $applied = '';
 my $cfname = '';
 if ( $cgi->param('cfname') =~ /^\w(\w|\s)*$/ ) {
 
@@ -104,15 +104,14 @@ if ( $cgi->param('cfname') =~ /^\w(\w|\s)*$/ ) {
   $twhere .= " AND CustomFields.Name = '$cfname'
     AND (ocfv_new.Id IS NOT NULL OR ocfv_old.Id IS NOT NULL OR ocfv_main.Id IS NOT NULL)";
 
-}
-else {
+} else {
+
   $transaction_time = "
   CASE transactions.type when 'Set'
     THEN (to_number(newvalue,'999999')-to_number(oldvalue, '999999')) * 60
     ELSE timetaken*60
   END";
  
-  my $applied = '';
   if ( $cgi->param('svcnum') =~ /^\s*(\d+)\s*$/ ) {
     $twhere .= " AND EXISTS( SELECT 1 FROM acct_rt_transaction WHERE acct_rt_transaction.transaction_id = Transactions.id AND svcnum = $1 )";
     $applied = "AND svcnum = $1";
@@ -122,13 +121,11 @@ else {
     AND (    ( Transactions.Type = 'Set'
                AND Transactions.Field = 'TimeWorked'
                AND Transactions.NewValue != Transactions.OldValue )
-          OR ( ( Transactions.Type='Create' OR Transactions.Type='Comment' OR Transactions.Type='Correspond' OR Transactions.Type='Touch' )
+          OR ( Transactions.Type IN ( 'Create', 'Comment', 'Correspond', 'Touch' )
                AND Transactions.TimeTaken > 0
              )
         )";
 
-  $applied_time = "( SELECT SUM(support) FROM acct_rt_transaction LEFT JOIN Transactions ON ( transaction_id = Id ) $twhere $applied )";
-
 }
 
 
@@ -155,9 +152,13 @@ my $ticket_time = "( SELECT SUM($transaction_time) $transactions )";
 push @select, "$ticket_time AS ticket_time";
 push @select_total, "SUM($ticket_time)";
 
-if ( $applied_time) {
+if ( $applied ) {
+
+  my $applied_time = "( SELECT SUM(support) FROM acct_rt_transaction LEFT JOIN Transactions ON ( transaction_id = Id ) $twhere $applied )";
+
   push @select, "$applied_time AS applied_time";
   push @select_total, "SUM($applied_time)";
+
 }
 
 my $query = {
index 1ae607b..eb250fb 100644 (file)
@@ -1,4 +1,4 @@
-<% include('elements/search.html',
+<& elements/search.html,
              'title'         => 'Time worked',
              'name_singular' => 'transaction',
              'query'         => $query,
@@ -29,8 +29,7 @@
                                   '',
                                   '',
                                 ],
-          )
-%>
+&>
 <%once>
 
 my $format_seconds_sub = sub {
index bf54469..71aa006 100644 (file)
@@ -1,9 +1,9 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                'title' => 'Query Results',
                'name'  => 'rows',
                'query' => "SELECT $sql",
-          )
-%>
+          
+&>
 <%init>
 
 die "access denied"
index 5363944..2298473 100644 (file)
@@ -51,7 +51,7 @@
 %       @{ $part_export->usage_sessions( {
 %            'stoptime_start'  => $beginning,
 %            'stoptime_end'    => $ending,
-%            'open_sessions'   => $open_sessions,
+%            'session_status'  => $status,
 %            'starttime_start' => $starttime_beginning,
 %            'starttime_end'   => $starttime_ending,
 %            'svc_acct'        => $cgi_svc_acct,
@@ -117,9 +117,9 @@ if ( $cgi->param('end') && $cgi->param('end') =~ /^(\d+)$/ ) {
   $ending = $1;
 }
 
-my $open_sessions = '';
-if ( $cgi->param('open_sessions') =~ /^(\d*)$/ ) {
-  $open_sessions = $1;
+my $status = '';
+if ( $cgi->param('session_status') =~ /^(closed|open)$/ ) {
+  $status = $1;
 }
 
 my( $starttime_beginning, $starttime_ending ) = ( '', '' );
@@ -242,8 +242,15 @@ my $time_format = sub {
   $pretty;
 };
 
+my $time_format_or_open = sub {
+  my $time = shift;
+  return '<CENTER>OPEN</CENTER>' if $time == 0;
+  &{$time_format}($time);
+};
+
 my $duration_format = sub {
   my $seconds = shift;
+  return '' if $seconds eq ''; # open session
   my $hour = int($seconds/3600);
   my $min = int( ($seconds%3600) / 60 );
   my $sec = $seconds%60;
@@ -339,7 +346,7 @@ tie %fields, 'Tie::IxHash',
   'acctstoptime'      => {
                            name    => 'End&nbsp;time',
                            attrib  => 'Acct-Stop-Time',
-                           fmt     => $time_format,
+                           fmt     => $time_format_or_open,
                            align   => 'left',
                          },
   'acctsessiontime'   => {
index 7b9fce3..547a9bb 100644 (file)
@@ -52,8 +52,9 @@
   <TR>
     <TD>Show:</TD>
     <TD>
-      <INPUT TYPE="radio" NAME="open_sessions" VALUE="0" onClick="open_changed(this);" CHECKED>Completed sessions<BR>
-      <INPUT TYPE="radio" NAME="open_sessions" VALUE="1" onClick="open_changed(this);">Open sessions
+      <INPUT TYPE="radio" NAME="session_status" VALUE="" onClick="enable_stop(true);" CHECKED>All sessions<BR>
+      <INPUT TYPE="radio" NAME="session_status" VALUE="closed" onClick="enable_stop(true);">Completed sessions<BR>
+      <INPUT TYPE="radio" NAME="session_status" VALUE="open" onClick="enable_stop(false);">Open sessions
     </TD>
   </TR>
 
 
   <SCRIPT TYPE="text/javascript">
 
-    function open_changed(what) {
-
-      var value=get_open_value(what); 
-      if ( value == '1' ) {
-        what.form.stoptime_beginning_text.disabled = true;
-        what.form.stoptime_ending_text.disabled = true;
-        what.form.stoptime_beginning_text.style.backgroundColor = '#dddddd';
-        what.form.stoptime_ending_text.style.backgroundColor = '#dddddd';
-        what.form.stoptime_beginning_button.style.display = 'none';
-        what.form.stoptime_ending_button.style.display = 'none';
-        what.form.stoptime_beginning_disabled.style.display = '';
-        what.form.stoptime_ending_disabled.style.display = '';
-      } else if ( value == '0' ) {
-        what.form.stoptime_beginning_text.disabled = false;
-        what.form.stoptime_ending_text.disabled = false;
-        what.form.stoptime_beginning_text.style.backgroundColor = '#ffffff';
-        what.form.stoptime_ending_text.style.backgroundColor = '#ffffff';
-        what.form.stoptime_beginning_button.style.display = '';
-        what.form.stoptime_ending_button.style.display = '';
-        what.form.stoptime_beginning_disabled.style.display = 'none';
-        what.form.stoptime_ending_disabled.style.display = 'none';
+    function enable_stop(value) {
+
+      var f = document.OneTrueForm;
+      if ( value ) {
+        f.stoptime_beginning_text.disabled = false;
+        f.stoptime_ending_text.disabled = false;
+        f.stoptime_beginning_text.style.backgroundColor = '#ffffff';
+        f.stoptime_ending_text.style.backgroundColor = '#ffffff';
+        f.stoptime_beginning_button.style.display = '';
+        f.stoptime_ending_button.style.display = '';
+        f.stoptime_beginning_disabled.style.display = 'none';
+        f.stoptime_ending_disabled.style.display = 'none';
+      } else {
+        f.stoptime_beginning_text.disabled = true;
+        f.stoptime_ending_text.disabled = true;
+        f.stoptime_beginning_text.style.backgroundColor = '#dddddd';
+        f.stoptime_ending_text.style.backgroundColor = '#dddddd';
+        f.stoptime_beginning_button.style.display = 'none';
+        f.stoptime_ending_button.style.display = 'none';
+        f.stoptime_beginning_disabled.style.display = '';
+        f.stoptime_ending_disabled.style.display = '';
       }
 
     }
 
-    function get_open_value(what) {
-      var rad_val = '';
-      for (var i=0; i < what.form.open_sessions.length; i++) {
-        if (what.form.open_sessions[i].checked) {
-          var rad_val = what.form.open_sessions[i].value;
-        }
-     }
-     return rad_val;
-   }
-
   </SCRIPT>
 
   <TR>
index 92e1c50..b9e5a7c 100755 (executable)
@@ -1,4 +1,4 @@
-<& elements/search.html,
+<& elements/svc_Common.html,
                  'title'       => emt('Account Search Results'),
                  'name'        => emt('accounts'),
                  'query'       => $sql_query,
index ee62e90..8366d21 100755 (executable)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/svc_Common.html,
               'title'       => 'Broadband Search Results',
               'name'        => 'broadband services',
               'html_init'   => $html_init,
@@ -49,8 +49,8 @@
                                  '',
                                  FS::UI::Web::cust_styles(),
                                ],
-          )
-%>
+          
+&>
 <%init>
 
 die "access denied" unless
@@ -72,7 +72,7 @@ else {
 }
 
 if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
-  $search_hash{'order_by'} = $1;
+  $search_hash{'order_by'} = "ORDER BY $1";
 }
 
 my $sql_query = FS::svc_broadband->search(\%search_hash);
index 94da035..1f8cbc3 100755 (executable)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/svc_Common.html,
                  'title'       => 'Dish Network Search Results',
                  'name'        => 'services',
                  'query'       => $sql_query,
@@ -34,8 +34,8 @@
                               '',
                               FS::UI::Web::cust_styles(),
                             ],
-             )
-%>
+             
+&>
 <%init>
 
 die "access denied"
@@ -61,7 +61,7 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
 my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
                 ' LEFT JOIN part_svc  USING ( svcpart ) '.
                 ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
-                ' LEFT JOIN cust_main USING ( custnum ) ';
+                FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
 
 #here is the agent virtualization
 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
index 9827b8d..56cfa30 100755 (executable)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'             => "Domain Search Results",
                  'name'              => 'domains',
                  'query'             => $sql_query,
@@ -34,8 +34,8 @@
                               '',
                               FS::UI::Web::cust_styles(),
                             ],
-              )
-%>
+              
+&>
 <%init>
 
 die "access denied"
@@ -66,7 +66,7 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
 my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
                 ' LEFT JOIN part_svc  USING ( svcpart ) '.
                 ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
-                ' LEFT JOIN cust_main USING ( custnum ) ';
+                FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
 
 #here is the agent virtualization
 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( 
index cb51d44..b282939 100755 (executable)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/svc_Common.html,
                  'title'             => 'External service search results',
                  'name'              => 'external services',
                  'query'             => $sql_query,
@@ -40,9 +40,8 @@
                               '',
                               FS::UI::Web::cust_styles(),
                             ],
-          )
-%>
-
+          
+&>
 <%init>
 
 die "access denied"
@@ -90,7 +89,7 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
 my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
                 ' LEFT JOIN part_svc  USING ( svcpart ) '.
                 ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
-                ' LEFT JOIN cust_main USING ( custnum ) ';
+                FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
 
 #here is the agent virtualization
 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
index f17f131..6a23bb3 100755 (executable)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'             => "Mail forward Search Results",
                  'name'              => 'mail forwards',
                  'query'             => $sql_query,
@@ -39,8 +39,8 @@
                               '',
                               FS::UI::Web::cust_styles(),
                             ],
-             )
-%>
+             
+&>
 <%init>
 
 die "access denied"
@@ -67,7 +67,7 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
 my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
                 ' LEFT JOIN part_svc  USING ( svcpart ) '.
                 ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
-                ' LEFT JOIN cust_main USING ( custnum ) ';
+                FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
 
 #here is the agent virtualization
 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( 
index ec09be8..93fc2c3 100644 (file)
@@ -1,4 +1,4 @@
-<% include('elements/search.html',
+<& elements/svc_Common.html,
             'title'             => 'Hardware service search results',
             'name'              => 'installations',
             'query'             => $sql_query,
@@ -34,8 +34,7 @@
                                       FS::UI::Web::cust_colors() ],
             'style'             => [ $svc_cancel_style, ('') x 7,
                                       FS::UI::Web::cust_styles() ],
-            )
-%>
+&>
 <%init>
 
 die "access denied"
@@ -44,8 +43,8 @@ die "access denied"
 my $addl_from = '
  LEFT JOIN cust_svc  USING ( svcnum  )
  LEFT JOIN part_svc  USING ( svcpart )
- LEFT JOIN cust_pkg  USING ( pkgnum  )
- LEFT JOIN cust_main USING ( custnum )
+ LEFT JOIN cust_pkg  USING ( pkgnum  )'.
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').'
  LEFT JOIN hardware_type USING ( typenum )';
 
 my @extra_sql;
index 2943408..f3a0564 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/svc_Common.html,
                  'title'             => "Phone number search results",
                  'name'              => 'phone numbers',
                  'query'             => $sql_query,
@@ -9,7 +9,7 @@
                                           'Country code',
                                           'Phone number',
                                           @header,
-                                          FS::UI::Web::cust_header(),
+                                          FS::UI::Web::cust_header($cgi->param('cust_fields')),
                                         ],
                  'fields'            => [ 'svcnum',
                                           'svc',
@@ -24,7 +24,7 @@
                                           $link,
                                           ( map '', @header ),
                                           ( map { $_ ne 'Cust. Status' ? $link_cust : '' }
-                                                FS::UI::Web::cust_header()
+                                                FS::UI::Web::cust_header($cgi->param('cust_fields'))
                                           ),
                                         ],
                  'align' => 'rlrr'.
@@ -46,8 +46,8 @@
                               ( map '', @header ),
                               FS::UI::Web::cust_styles(),
                             ],
-              )
-%>
+              
+&>
 <%init>
 
 die "access denied"
@@ -56,8 +56,6 @@ die "access denied"
 my $conf = new FS::Conf;
 
 my @select = ();
-my %svc_phone = ();
-my @extra_sql = ();
 my $orderby = 'ORDER BY svcnum';
 
 my @header = ();
@@ -65,9 +63,12 @@ my @fields = ();
 my $link = [ "${p}view/svc_phone.cgi?", 'svcnum' ];
 my $redirect = $link;
 
+my %search_hash = ();
+my @extra_sql = ();
+
 if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
 
-  push @extra_sql, 'pkgnum IS NULL'
+  $search_hash{'unlinked'} = 1
     if $cgi->param('magic') eq 'unlinked';
 
   if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
@@ -119,52 +120,31 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
 
   }
 
+} elsif ( $cgi->param('magic') =~ /^advanced$/ ) {
+
+  for (qw( agentnum custnum cust_status balance balance_days cust_fields )) {
+    $search_hash{$_} = $cgi->param($_) if length($cgi->param($_));
+  }
+
+  for (qw( payby pkgpart svcpart )) {
+    $search_hash{$_} = [ $cgi->param($_) ] if $cgi->param($_);
+  }
+
 } elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
-  push @extra_sql, "svcpart = $1";
+  $search_hash{'svcpart'} = [ $1 ];
 } else {
   $cgi->param('phonenum') =~ /^([\d\- ]+)$/; 
-  ( $svc_phone{'phonenum'} = $1 ) =~ s/\D//g;
+  my $phonenum = $1;
+  $phonenum =~ s/\D//g;
+  push @extra_sql, "phonenum = '$phonenum'";
 }
 
-my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
-                ' LEFT JOIN part_svc  USING ( svcpart ) '.
-                ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
-                ' LEFT JOIN cust_main USING ( custnum ) ';
-
-#here is the agent virtualization
-push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
-                   'null_right' => 'View/link unlinked services'
-                 );
-
-my $extra_sql = '';
-if ( @extra_sql ) {
-  $extra_sql = ( keys(%svc_phone) ? ' AND ' : ' WHERE ' ).
-               join(' AND ', @extra_sql );
-}
+$search_hash{'addl_select'} = \@select;
+$search_hash{'order_by'} = $orderby;
+$search_hash{'where'} = \@extra_sql;
 
-my $count_query = "SELECT COUNT(*) FROM svc_phone $addl_from ";
-if ( keys %svc_phone ) {
-  $count_query .= ' WHERE '.
-                    join(' AND ', map "$_ = ". dbh->quote($svc_phone{$_}),
-                                      keys %svc_phone
-                        );
-}
-$count_query .= $extra_sql;
-
-my $sql_query = {
-  'table'     => 'svc_phone',
-  'hashref'   => \%svc_phone,
-  'select'    => join(', ',
-                   'svc_phone.*',
-                   'part_svc.svc',
-                   @select,
-                   'cust_main.custnum',
-                   FS::UI::Web::cust_sql_fields(),
-                 ),
-  'extra_sql' => $extra_sql,
-  'order_by'  => $orderby,
-  'addl_from' => $addl_from,
-};
+my $sql_query = FS::svc_phone->search(\%search_hash);
+my $count_query = delete($sql_query->{'count_query'});
 
 #smaller false laziness w/svc_*.cgi here
 my $link_cust = sub {
index adc31c8..7410262 100755 (executable)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/svc_Common.html,
                  'title'       => 'Virtual Host Search Results',
                  'name'        => 'virtual hosts',
                  'query'       => $sql_query,
@@ -45,8 +45,8 @@
                               '',
                               FS::UI::Web::cust_styles(),
                             ],
-             )
-%>
+             
+&>
 <%init>
 
 die "access denied"
@@ -73,7 +73,7 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
 my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
                 ' LEFT JOIN part_svc  USING ( svcpart ) '.
                 ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
-                ' LEFT JOIN cust_main USING ( custnum ) ';
+                FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
 
 #here is the agent virtualization
 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
index bbfd033..fa4b895 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
                  'title'       => 'Time Worked',
                  'name'        => 'time',
                 'html_form'   => qq!<FORM NAME="timeForm" ACTION="${p}misc/timeworked.html" METHOD="POST">!,
@@ -33,9 +33,8 @@
                    '',
                  ],
                  'html_foot' => $html_foot,
-             )
-
-%>
+             
+&>
 <%init>
 
 die "access denied"
index f61de05..285fb50 100644 (file)
@@ -114,13 +114,12 @@ if ( $cgi->param('status') =~ /^([a-z]+)$/ ) {
 push @where, "cust_bill._date >= $beginning",
              "cust_bill._date <= $ending";
 
-if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
-  push @where, "cust_main.agentnum = $1";
-}
-
-if ( $cgi->param('cust_classnum') ) {
-  my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
-  push @where, 'cust_main.classnum IN('.join(',',@classnums).')'
+# cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html, cust_bill_pkg_referral.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
+if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
+  my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
+  push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
+                   join(',', map { $_ || '0' } @classnums ).
+               ' )'
     if @classnums;
 }
 
@@ -210,8 +209,8 @@ push @select, '(edate - 82799) AS before_edate';
 #usage always excluded
 
 # always 'nottax', not 'istax'
-$join_cust =  '        JOIN cust_bill USING ( invnum )
-                  LEFT JOIN cust_main USING ( custnum ) ';
+$join_cust =  '        JOIN cust_bill USING ( invnum ) '.
+                  FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
 
 $join_pkg .=  ' LEFT JOIN cust_pkg USING ( pkgnum )
                 LEFT JOIN part_pkg USING ( pkgpart )
@@ -222,7 +221,7 @@ my $where = ' WHERE '. join(' AND ', @where);
 
 my $count_query = "SELECT COUNT(DISTINCT billpkgnum), 
   SUM( $unearned_base ), SUM( $unearned_sql )
-  FROM cust_bill_pkg $join_cust $join_pkg $where";
+  FROM cust_bill_pkg $join_pkg $join_cust $where";
 
 push @select, 'part_pkg.pkg',
               'part_pkg.freq',
@@ -231,7 +230,7 @@ push @select, 'part_pkg.pkg',
 
 my $query = {
   'table'     => 'cust_bill_pkg',
-  'addl_from' => "$join_cust $join_pkg",
+  'addl_from' => "$join_pkg $join_cust",
   'hashref'   => {},
   'select'    => join(",\n", @select ),
   'extra_sql' => $where,
index f85e4fb..a7791ba 100644 (file)
@@ -1,4 +1,4 @@
-<% include( 'elements/search.html',
+<& elements/search.html,
               'title'         => 'Unprovisioned Service Search Results',
               'name' => 'packages with unprovisioned services',
               'query'         => {
@@ -54,8 +54,8 @@
                              '',
                               FS::UI::Web::cust_styles(),
                            ],
-      )
-%>
+      
+&>
 <%init>
 
 die "access denied"
@@ -74,7 +74,8 @@ my $search = " where cust_pkg.cancel is null and pkg_svc.quantity > 0 and "
            . " cust_svc.pkgnum = cust_pkg.pkgnum and "
            . " cust_svc.svcpart = pkg_svc.svcpart) $svcpart_limit";
 
-my $addl_from = " join pkg_svc using (pkgpart) join cust_main using (custnum) ";
+my $addl_from = " join pkg_svc using (pkgpart) ".
+            FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
 
 # this was very painful to derive but it appears correct
 #select cust_pkg.custnum,cust_pkg.pkgpart,cust_pkg.pkgnum, pkg_svc.svcpart from cust_pkg join
index 7d64039..55ee4be 100644 (file)
@@ -13,7 +13,7 @@
                                'hashref' => { },
                                'addl_from' => 
                                  'LEFT JOIN cust_bill USING ( invnum ) '.
-                                 'LEFT JOIN cust_main USING ( custnum )',
+                                 FS::UI::Web::join_cust_main('cust_bill'),
                                'extra_sql' => " WHERE batchnum = $batchnum",
                              },
               'count_query' => "SELECT COUNT(*) FROM cust_bill_batch WHERE batchnum = $batchnum",
index 5c46803..b863a73 100644 (file)
   <TD ALIGN="right"><% mt('Email address(es)') |h %></TD>
   <TD BGCOLOR="#ffffff">
     <% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) || $no %>
+%   if ( $cust_main->message_noemail ) {
+    <BR>
+    <SPAN STYLE="font-size: small"><% emt('(do not send notices)') %></SPAN>
+%   }
   </TD>
 </TR>
 % }
index ea84b8f..bf32a49 100644 (file)
@@ -43,10 +43,12 @@ tie my %tables, 'Tie::IxHash',
   'svc_external'      => 'External service',
   'svc_phone'         => 'Phone',
   'phone_device'      => 'Phone device',
+  'cust_pkg_discount' => 'Discount',
   #? it gets provisioned anyway 'phone_avail'         => 'Phone',
 ;
 
-my $svc_join = 'JOIN cust_svc USING ( svcnum ) JOIN cust_pkg USING ( pkgnum )';
+my $pkg_join = "JOIN cust_pkg USING ( pkgnum )";
+my $svc_join = "JOIN cust_svc USING ( svcnum ) $pkg_join";
 
 my %table_join = (
   'svc_acct'         => $svc_join,
@@ -58,6 +60,7 @@ my %table_join = (
   'svc_external'     => $svc_join,
   'svc_phone'        => $svc_join,
   'phone_device'     => $svc_join,
+  'cust_pkg_discount'=> $pkg_join,
 );
 
 
@@ -104,7 +107,7 @@ my $conf = new FS::Conf;
 
 my $curuser = $FS::CurrentUser::CurrentUser;
 
-die "access deined"
+die "access denied"
   unless $curuser->access_right('View customer history');
 
 # find out the beginning of this customer history, if possible
index 7d79306..546dd89 100755 (executable)
@@ -1,3 +1,29 @@
+<STYLE TYPE="text/css">
+td.package {
+  vertical-align: top;
+  border-width: 0;
+  border-style: solid;
+  border-color: #bbbbff;
+}
+table.package {
+  border: none;
+  padding: 0;
+  border-spacing: 0;
+  width: 100%;
+}
+table.usage {
+  border: 1px solid black;
+  margin: auto;
+  width: 60%;
+  border-spacing: 0px;
+}
+.shared > * {
+  background-color: #ffffaa;
+}
+.row0 { background-color: #eeeeee; }
+.row1 { background-color: #ffffff; }
+
+</STYLE>
 % my $s = 0;
 
 % if ( $curuser->access_right('Qualify service') ) { 
 
   <TR>
     <TD COLSPAN=2>
-% if ( $conf->exists('cust_pkg-group_by_location') and $show_location ) {
+% if ( $conf->exists('cust_pkg-group_by_location') ) {
 <& locations.html,
     'cust_main'     => $cust_main,
     'packages'      => $packages,
 <& packages/section.html,
     'cust_main'     => $cust_main,
     'packages'      => $packages,
-    'show_location' => $show_location,
  &>
 </TABLE>
 % }
@@ -114,10 +139,6 @@ my $curuser = $FS::CurrentUser::CurrentUser;
 
 my( $packages, $num_old_packages ) = get_packages($cust_main, $conf);
 
-
-my $show_location = $conf->exists('cust_pkg-always_show_location') 
-                        || (grep $_->locationnum, @$packages); # ? '1' : '0';
-
 my $countrydefault = scalar($conf->config('countrydefault')) || 'US';
 #subroutines
 
@@ -178,6 +199,10 @@ sub get_packages {
   }
 
   $num_old_packages -= scalar(@packages);
+  
+  # don't include supplemental packages in this list; they'll be found from
+  # their main packages
+  @packages = grep !$_->main_pkgnum, @packages;
 
   ( \@packages, $num_old_packages );
 }
diff --git a/httemplate/view/cust_main/packages/contact.html b/httemplate/view/cust_main/packages/contact.html
new file mode 100644 (file)
index 0000000..9312991
--- /dev/null
@@ -0,0 +1,61 @@
+% if ( $contact ) {
+    <% $contact->line |h %>
+% if ( $show_link ) {
+        <FONT SIZE=-1>
+          (&nbsp;<%pkg_change_contact_link($cust_pkg)%>&nbsp;)
+        </FONT>
+%    }
+% } elsif ( $show_link ) {
+        <FONT SIZE=-1>
+          (&nbsp;<%pkg_add_contact_link($cust_pkg)%>&nbsp;)
+        </FONT>
+% }
+<%init>
+
+my $conf = new FS::Conf;
+my %opt = @_;
+
+my $cust_pkg       = $opt{'cust_pkg'};
+
+my $show_link = 
+  ! $cust_pkg->get('cancel')
+  && $FS::CurrentUser::CurrentUser->access_right('Change customer package');
+
+my $contact = $cust_pkg->contact_obj;
+
+sub pkg_change_contact_link {
+  my $cust_pkg = shift;
+  #my $pkgpart = $cust_pkg->pkgpart;
+  include( '/elements/popup_link-cust_pkg.html',
+    'action'      => $p. "misc/change_pkg_contact.html",
+    'label'       => emt('Change'), # contact'),
+    'actionlabel' => emt('Change'),
+    'cust_pkg'    => $cust_pkg,
+    'width'       => 616,
+    'height'      => 220,
+  );
+}
+
+sub pkg_add_contact_link {
+  my $cust_pkg = shift;
+  #my $pkgpart = $cust_pkg->pkgpart;
+  include( '/elements/popup_link-cust_pkg.html',
+    'action'      => $p. "misc/change_pkg_contact.html",
+    'label'       => emt('Add contact'),
+    'actionlabel' => emt('Change'),
+    'cust_pkg'    => $cust_pkg,
+    'width'       => 616,
+    'height'      => 192,
+  );
+}
+
+#sub edit_contact_link {
+#  my $contactnum = shift;
+#  include( '/elements/popup_link.html',
+#    'action'      => $p. "edit/cust_contact.cgi?contactnum=$contactnum",
+#    'label'       => emt('Edit contact'),
+#    'actionlabel' => emt('Edit'),
+#  );
+#}
+
+</%init>
index 34e3a64..f2d3798 100644 (file)
@@ -1,7 +1,5 @@
-<TD CLASS="inv" BGCOLOR="<% $bgcolor %>" WIDTH="20%">
-
-% unless ( $cust_pkg->locationnum ) {
-  <I><FONT SIZE=-1>(<% mt('default service address') |h %>)</FONT><BR>
+% if ( $default ) {
+  <DIV STYLE="font-style: italic; font-size: small">
 % }
 
     <% $loc->location_label( 'join_string'     => '<BR>',
@@ -24,8 +22,8 @@
         </FONT>
 %   }
 
-% unless ( $cust_pkg->locationnum ) {
-  </I>
+% if ( $default ) {
+  </DIV>
 % }
 
 % if ( ! $cust_pkg->get('cancel')
   </FONT>
 % } 
 
-</TD>
 <%init>
 
 my $conf = new FS::Conf;
 my %opt = @_;
 
-my $bgcolor        = $opt{'bgcolor'};
 my $cust_pkg       = $opt{'cust_pkg'};
 my $countrydefault = $opt{'countrydefault'} || 'US';
 my $statedefault   = $opt{'statedefault'}
                      || ($countrydefault eq 'US' ? 'CA' : '');
 
 my $loc = $cust_pkg->cust_location_or_main;
+# dubious--they should all have a location now
+my $default = $cust_pkg->locationnum == $opt{'cust_main'}->ship_locationnum;
 
 sub pkg_change_location_link {
   my $cust_pkg = shift;
index 5d93ad4..520305a 100644 (file)
@@ -1,5 +1,6 @@
-<TD CLASS="inv" BGCOLOR="<% $bgcolor %>" VALIGN="top">
-  <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
+<TD CLASS="inv package" BGCOLOR="<% $bgcolor %>" VALIGN="top"
+  STYLE="border-left-width: <% $supplemental * 30 %>px">
+  <TABLE CLASS="inv package"> 
     <TR>
       <TD COLSPAN=2>
         <A NAME="cust_pkg<% $cust_pkg->pkgnum %>"
         <B><% $cust_pkg->quantity %></B>
       </TD>
     </TR>
-%  }
+% }
 
     <TR>
       <TD COLSPAN=2>
         <FONT SIZE=-1>
 
-%         unless ( $cust_pkg->get('cancel') ) { 
+%         unless ( $cust_pkg->get('cancel') ) {
 %
-%           my $br = 0;
-%           if ( $curuser->access_right('Change customer package') ) {
-%             $br=1;
-              (&nbsp;<%pkg_change_link($cust_pkg)%>&nbsp;)
-%           } 
+%           if ( $supplemental or $part_pkg->freq eq '0' ) {
+%             # Supplemental packages can't be changed independently.
+%             # One-time charges don't need to be changed.
+%             # For both of those, we only show "Edit dates", "Add comments",
+%             # and "Add invoice details".
+%             if ( $curuser->access_right('Edit customer package dates') ) {
+                (&nbsp;<%pkg_dates_link($cust_pkg)%>&nbsp;)
+%             }
+%           } else {
+%             # the usual case: links to change package definition,
+%             # discount, and customization
+%             my $br = 0;
+%             if ( $curuser->access_right('Change customer package') ) {
+%               $br=1;
+                (&nbsp;<%pkg_change_link($cust_pkg)%>&nbsp;)
+%             } 
 %
-%           if ( $curuser->access_right('Edit customer package dates') ) {
-%             $br=1;
-              (&nbsp;<%pkg_dates_link($cust_pkg)%>&nbsp;)
-%           } 
+%             if ( $curuser->access_right('Edit customer package dates') ) {
+%               $br=1;
+                (&nbsp;<%pkg_dates_link($cust_pkg)%>&nbsp;)
+%             
 %
-%           if ( $curuser->access_right('Discount customer package')
-%                && $part_pkg->can_discount
-%                && ! scalar($cust_pkg->cust_pkg_discount_active)
-%                && ! scalar($cust_pkg->part_pkg->part_pkg_discount)
-%              )
-%           {
-%             $br=1;
-              (&nbsp;<%pkg_discount_link($cust_pkg)%>&nbsp;)
-%           }
+%             if ( $curuser->access_right('Discount customer package')
+%                  && $part_pkg->can_discount
+%                  && ! scalar($cust_pkg->cust_pkg_discount_active)
+%                  && ! scalar($cust_pkg->part_pkg->part_pkg_discount)
+%                )
+%             {
+%               $br=1;
+                (&nbsp;<%pkg_discount_link($cust_pkg)%>&nbsp;)
+%             }
 %
-%           if ( $curuser->access_right('Customize customer package') ) {
-%             $br=1;
-              (&nbsp;<%pkg_customize_link($cust_pkg,$part_pkg)%>&nbsp;)
-%           } 
+%             if ( $curuser->access_right('Customize customer package') ) {
+%               $br=1;
+                (&nbsp;<%pkg_customize_link($cust_pkg,$part_pkg)%>&nbsp;)
+%             
 %
-            <% $br ? '<BR>' : '' %>
-%         } 
+              <% $br ? '<BR>' : '' %>
+%           
 
-%         if ( $cust_pkg->num_cust_event
-%              && (    $curuser->access_right('Billing event reports')
-%                   || $curuser->access_right('View customer billing events')
-%                 )
-%            ) {
-            (&nbsp;<%pkg_event_link($cust_pkg)%>&nbsp;)
-%         }
+%           if ( $cust_pkg->num_cust_event
+%                && (    $curuser->access_right('Billing event reports')
+%                     || $curuser->access_right('View customer billing events')
+%                   )
+%              ) {
+              (&nbsp;<%pkg_event_link($cust_pkg)%>&nbsp;)
+%           }
+%         } #!$supplemental
 
         </FONT>
       </TD>
       </TR>
 %     if ( $curuser->access_right('Change customer package') and 
 %           !$cust_pkg->get('cancel') and
-%           !$opt{'show_location'}) {
+%           !$supplemental and
+%           $part_pkg->freq ne '0' ) {
       <TR>
+%       if ( FS::Conf->new->exists('invoice-unitprice') ) {
         <TD><FONT SIZE="-1">
-          (&nbsp;<% pkg_change_location_link($cust_pkg) %>&nbsp;)
+          (&nbsp;<% pkg_change_quantity_link($cust_pkg) %>&nbsp;)
         </FONT></TD>
+%       }
       </TR>
 %     }
 %   }
   </TABLE>
+% if ( @cust_pkg_usage ) {
+  <TABLE CLASS="usage inv">
+    <TR><TH COLSPAN=4><% mt('Included usage') %></TH></TR>
+%   foreach my $usage (@cust_pkg_usage) {
+%     my $part = $usage->part_pkg_usage;
+%     my $ratio = 255 * ($usage->minutes / $part->minutes);
+%     $ratio = 255 if $ratio > 255; # because rollover
+%     my $color = sprintf('STYLE="font-weight: bold; color: #%02x%02x00"', 255 - $ratio, $ratio);
+%     my $trstyle = '';
+%     $trstyle = ' CLASS="shared"' if $part->shared;
+    <TR<%$trstyle%>>
+      <TD ALIGN="right"><% $part->description %>: </TD>
+      <TD <%$color%> ALIGN="right"><% $usage->minutes %></TD>
+      <TD <%$color%>> / </TD>
+      <TD <%$color%>><% $part->minutes %></TD>
+%     if ( $part->shared ) {
+      <TD><I>(shared)</I></TD>
+%     }
+    </TR>
+%   }
+  </TABLE>
+% }
 
 </TD>
 
@@ -196,6 +234,18 @@ my $countrydefault = $opt{'countrydefault'} || 'US';
 my $statedefault   = $opt{'statedefault'}
                      || ($countrydefault eq 'US' ? 'CA' : '');
 
+my $supplemental = $opt{'supplemental'} || 0;
+
+$cust_pkg->pkgnum =~ /^(\d+)$/;
+my $pkgnum = $1;
+my @cust_pkg_usage = qsearch({
+  'select'    => 'cust_pkg_usage.*',
+  'table'     => 'cust_pkg_usage',
+  'addl_from' => ' JOIN part_pkg_usage USING (pkgusagepart)',
+  'extra_sql' => " WHERE pkgnum = $1",
+  'order_by'  => ' ORDER BY priority ASC, description ASC',
+});
+
 #subroutines
 
 #false laziness w/status.html
@@ -229,6 +279,17 @@ sub pkg_change_location_link {
   );
 }
 
+sub pkg_change_quantity_link {
+  include( '/elements/popup_link-cust_pkg.html',
+    'action'      => $p. 'edit/cust_pkg_quantity.html?',
+    'label'       => emt('Change quantity'),
+    'actionlabel' => emt('Change'),
+    'cust_pkg'    => shift,
+    'width'       => 390,
+    'height'      => 220,
+  );
+}
+
 sub pkg_dates_link { pkg_link('edit/REAL_cust_pkg', emt('Edit dates'), @_ ); }
 
 sub pkg_discount_link {
index 85f0c79..5f54c0a 100755 (executable)
@@ -1,53 +1,48 @@
 % if ( @$packages ) { 
-%   my $bgcolor1 = '#eeeeee';
-%   my $bgcolor2 = '#ffffff';
-%   my $bgcolor = '';
-
 <TR>
 % #my $width = $show_location ? 'WIDTH="25%"' : 'WIDTH="33%"';
   <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Package') |h %></TH>
   <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Status') |h %></TH>
-%   if ( $show_location ) {
-  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Location') |h %></TH>
-% }
+  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Contact/Location') |h %></TH>
   <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Services') |h %></TH>
 </TR>
 
 % #$FS::cust_pkg::DEBUG = 2;
 %   foreach my $cust_pkg (@$packages) {
+    <& .packagerow, $cust_pkg,
+        'cust_main' => $opt{'cust_main'},
+        'bgcolor'   => $opt{'bgcolor'},
+        %conf_opt
+    &>
+%   }
+% } else { # there are no packages
+<BR>
+% }
+<%def .packagerow>
 %
-%     if ( $bgcolor eq $bgcolor1 ) {
-%       $bgcolor = $bgcolor2;
-%     } else {
-%       $bgcolor = $bgcolor1;
-%     }
-%
-%     my %iopt = (
-%       'bgcolor'   => $bgcolor,
-%       'cust_pkg'  => $cust_pkg,
-%       'part_pkg'  => $cust_pkg->part_pkg,
-%       'cust_main' => $opt{'cust_main'},
-%       %conf_opt,
-%     );
-%
-
+% my ($cust_pkg, %iopt) = @_;
+% $iopt{'cust_pkg'} = $cust_pkg;
+% $iopt{'part_pkg'} = $cust_pkg->part_pkg;
   <!--pkgnum: <% $cust_pkg->pkgnum %>-->
-  <TR>
+  <TR CLASS="row<%$row % 2%>">
     <& package.html, %iopt &>
-    <& status.html, %iopt &>
-%     if ( $show_location ) {
-    <& location.html, %iopt &>
-%     }
+    <& status.html,  %iopt &>
+    <TD CLASS="inv" BGCOLOR="<% $iopt{bgcolor} %>" WIDTH="20%" VALIGN="top">
+      <& contact.html, %iopt &>
+      <& location.html, %iopt &>
+    </TD>
     <& services.html, %iopt &>
   </TR>
-
-%   } #foreach $cust_pkg
-%# </TABLE>
-% } #if @$packages
-% else {
-<BR>
+% $row++;
+% # include supplemental packages if any
+% $iopt{'supplemental'} = ($iopt{'supplemental'} || 0) + 1;
+% foreach my $supp_pkg ($cust_pkg->supplemental_pkgs) {
+    <& .packagerow, $supp_pkg, %iopt &>
 % }
-
+</%def>
+<%shared>
+my $row = 0;
+</%shared>
 <%init>
 
 my %opt = @_;
@@ -56,7 +51,6 @@ my $conf = new FS::Conf;
 my $curuser = $FS::CurrentUser::CurrentUser;
 
 my $packages = $opt{'packages'};
-my $show_location = $opt{'show_location'};
 
 # Sort order is hardcoded for now, can change this if needed.
 @$packages = sort { 
@@ -89,10 +83,8 @@ my %conf_opt = (
   'manage_link_loc'           => scalar($conf->config('svc_broadband-manage_link_loc')),
   'manage_link-new_window'    => $conf->exists('svc_broadband-manage_link-new_window'),
   'maestro-status_test'       => $conf->exists('maestro-status_test'),
-  'cust_pkg-large_pkg_size'   => $conf->config('cust_pkg-large_pkg_size'),
+  'cust_pkg-large_pkg_size'   => scalar($conf->config('cust_pkg-large_pkg_size')),
 
-  # for packages.html Change location link
-  'show_location'             => $show_location,
 );
 
 </%init>
index e901774..9d5a88e 100644 (file)
@@ -1,9 +1,11 @@
-<TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
+<TD CLASS="inv" BGCOLOR="<% $bgcolor %>" VALIGN="top">
   <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
 
 %#this should use cust_pkg->status and cust_pkg->statuscolor eventually
 
-% if ( $cust_pkg->order_date ) {
+% if ( $supplemental ) {
+    <% pkg_status_row_colspan($cust_pkg, emt('Supplemental'), '', 'color' => '7777FF', %opt) %>
+% } elsif ( $cust_pkg->order_date ) {
     <% pkg_status_row($cust_pkg, emt('Ordered'), 'order_date', %opt ) %>
 % }
 
 
     <% pkg_status_row($cust_pkg, emt('Cancelled'), 'cancel', 'color'=>'FF0000', %opt ) %>
 
-    <% pkg_status_row_colspan( $cust_pkg,
-         ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '',
-         'align'=>'right', 'color'=>'ff0000', 'size'=>'-2', 'colspan'=>$colspan,
-         %opt
-       )
-    %>
+    <% pkg_reason_row($cust_pkg, $cpr, color => 'ff0000', %opt) %>
 
 %   unless ( $cust_pkg->get('setup') ) { 
 
-        <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', 'colspan'=>$colspan, %opt, ) %>
+        <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', %opt, ) %>
 
 %   } else { 
 
        <% pkg_status_row( $cust_pkg, emt('Setup'), 'setup', %opt ) %>
-       <% pkg_status_row_changed( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+       <% pkg_status_row_changed( $cust_pkg, %opt ) %>
        <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %>
        <% pkg_status_row_if( $cust_pkg, emt('Suspended'), 'susp', %opt, curuser=>$curuser ) %>
 
 %   } 
 %
-%   if ( $part_pkg->freq ) { #?
+%   if ( $part_pkg->freq and !$supplemental ) { #?
 
       <TR>
-        <TD COLSPAN=<%$colspan%>>
+        <TD COLSPAN=<%$opt{colspan}%>>
           <FONT SIZE=-1>
 %           if ( $curuser->access_right('Un-cancel customer package') ) { 
               (&nbsp;<% pkg_uncancel_link($cust_pkg) %>&nbsp;)
 
     <% pkg_status_row( $cust_pkg, emt('Suspended'), 'susp', 'color'=>'FF9900', %opt ) %>
 
-    <% pkg_status_row_colspan( $cust_pkg,
-         ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '',
-         'align'=>'right', 'color'=>'FF9900', 'size'=>'-2', 'colspan'=>$colspan,
-         %opt,
-       )
-    %>
+    <% pkg_reason_row( $cust_pkg, $cpr, 'color' => 'FF9900', %opt ) %>
 
-    <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+    <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
 
-    <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+    <% pkg_status_row_discount( $cust_pkg, %opt ) %>
 
 %   unless ( $cust_pkg->get('setup') ) { 
-      <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', 'colspan'=>$colspan, %opt ) %>
+      <% pkg_status_row_colspan( $cust_pkg, emt('Never billed'), '', %opt ) %>
 %   } else { 
       <% pkg_status_row($cust_pkg, emt('Setup'), 'setup', %opt ) %>
 %   } 
 
     <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
 
-    <% pkg_status_row_changed( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+    <% pkg_status_row_changed( $cust_pkg, %opt ) %>
     <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %>
 %   if ( $cust_pkg->option('suspend_bill', 1)
 %        || ( $part_pkg->option('suspend_bill', 1)
     <% pkg_status_row_if( $cust_pkg, emt('Expires'), 'expire', %opt, curuser=>$curuser ) %>
     <% pkg_status_row_if( $cust_pkg, emt('Contract ends'), 'contract_end', %opt ) %>
 
-    <TR>
-      <TD COLSPAN=<%$colspan%>>
-        <FONT SIZE=-1>
-%         if ( $curuser->access_right('Unsuspend customer package') ) { 
-            (&nbsp;<% pkg_unsuspend_link($cust_pkg) %>&nbsp;)
-            (&nbsp;<% pkg_resume_link($cust_pkg) %>&nbsp;)
-%         }
-%         if ( $curuser->access_right('Cancel customer package immediately') ) {
-            (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
-%         } 
-        </FONT>
-      </TD>
-    </TR>
-
+% if ( !$supplemental ) {
+      <TR>
+        <TD COLSPAN=<%$opt{colspan}%>>
+          <FONT SIZE=-1>
+%           if ( $curuser->access_right('Unsuspend customer package') ) { 
+              (&nbsp;<% pkg_unsuspend_link($cust_pkg) %>&nbsp;)
+              (&nbsp;<% pkg_resume_link($cust_pkg) %>&nbsp;)
+%           }
+%           if ( $curuser->access_right('Cancel customer package immediately') ) {
+              (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
+%           } 
+          </FONT>
+        </TD>
+      </TR>
+%     }
+%
 %   } else { #status: active
 %
 %     unless ( $cust_pkg->get('setup') ) { #not setup
 %
 %       unless ( $part_pkg->freq ) {
 
-          <% pkg_status_row_colspan( $cust_pkg, emt('Not yet billed (one-time charge)'), '', 'colspan'=>$colspan, %opt ) %>
+          <% pkg_status_row_colspan( $cust_pkg, emt('Not yet billed (one-time charge)'), '', %opt ) %>
 
-          <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
 
-          <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_discount( $cust_pkg, %opt ) %>
 
           <% pkg_status_row_if(
                $cust_pkg,
 
           <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
 
+%         if (!$supplemental) {
           <TR>
-            <TD COLSPAN=<%$colspan%>>
+            <TD COLSPAN=<%$opt{colspan}%>>
               <FONT SIZE=-1>
 %               if ( $curuser->access_right('Cancel customer package immediately') ) { 
                   (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
               </FONT>
             </TD>
           </TR>
+%         }
 
 %       } else { 
 
-          <% pkg_status_row_colspan($cust_pkg, emt("Not yet billed ($billed_or_prepaid [_1])", myfreq($part_pkg) ), '', 'colspan'=>$colspan, %opt ) %>
+          <% pkg_status_row_colspan($cust_pkg, emt("Not yet billed ($billed_or_prepaid [_1])", myfreq($part_pkg) ), '', %opt ) %>
 
-          <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
 
-          <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_discount( $cust_pkg, %opt ) %>
 
           <% pkg_status_row_if($cust_pkg, emt('Start billing'), 'start_date', %opt) %>
           <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
 %
 %       unless ( $part_pkg->freq ) { 
 
-          <% pkg_status_row_colspan($cust_pkg, emt('One-time charge'), '', 'colspan'=>$colspan, %opt ) %>
+          <% pkg_status_row_colspan($cust_pkg, emt('One-time charge'), '', %opt ) %>
 
           <% pkg_status_row($cust_pkg, emt('Billed'), 'setup', %opt) %>
 
-          <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
 
-          <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_discount( $cust_pkg, %opt ) %>
 
           <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
 
             <% pkg_status_row_colspan( $cust_pkg,
                  emt('Overlimit'),
                  $billed_or_prepaid. '&nbsp;'. myfreq($part_pkg),
-                 'color'=>'FFD000', 'colspan'=>$colspan,
+                 'color'=>'FFD000',
                  %opt
                )
             %>
             <% pkg_status_row_colspan( $cust_pkg,
                  emt('Active'),
                  $billed_or_prepaid. '&nbsp;'. myfreq($part_pkg),
-                 'color'=>'00CC00', 'colspan'=>$colspan,
+                 'color'=>'00CC00',
                  %opt
                )
             %>
 %         } 
 
-          <% pkg_status_row_noauto( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_noauto( $cust_pkg, %opt ) %>
 
-          <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+          <% pkg_status_row_discount( $cust_pkg, %opt ) %>
 
           <% pkg_status_row($cust_pkg, emt('Setup'), 'setup', %opt) %>
 
 %       $cust_pkg->set('autosuspend', $autosuspend) if $autosuspend;
 %     }
 
-      <% pkg_status_row_changed( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
+      <% pkg_status_row_changed( $cust_pkg, %opt ) %>
       <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %>
       <% pkg_status_row_if( $cust_pkg, $next_bill_or_prepaid_until, 'bill', %opt, curuser=>$curuser ) %>
       <% pkg_status_row_if($cust_pkg, emt('Will automatically suspend by'), 'autosuspend', %opt) %>
       <% pkg_status_row_if( $cust_pkg, emt('Expires'), 'expire', %opt, curuser=>$curuser ) %>
       <% pkg_status_row_if( $cust_pkg, emt('Contract ends'), 'contract_end', %opt ) %>
 
-%     if ( $part_pkg->freq ) { 
+%     if ( $part_pkg->freq and !$supplemental ) { 
 
         <TR>
-          <TD COLSPAN=<%$colspan%>>
+          <TD COLSPAN=<%$opt{colspan}%>>
             <FONT SIZE=-1>
 %             if ( $curuser->access_right('Suspend customer package') ) { 
                 (&nbsp;<% pkg_suspend_link($cust_pkg) %>&nbsp;)
@@ -251,8 +247,10 @@ my $bgcolor  = $opt{'bgcolor'};
 my $cust_pkg = $opt{'cust_pkg'};
 my $part_pkg = $opt{'part_pkg'};
 my $curuser  = $FS::CurrentUser::CurrentUser;
-my $colspan  = $opt{'cust_pkg-display_times'} ? 8 : 4;
 my $width    = $opt{'cust_pkg-display_times'} ? '38%' : '56%';
+my $supplemental = $opt{'supplemental'};
+
+$opt{colspan}  = $opt{'cust_pkg-display_times'} ? 8 : 4;
 
 #false laziness w/edit/REAL_cust_pkg.cgi
 my( $billed_or_prepaid, $last_bill_or_renewed, $next_bill_or_prepaid_until );
@@ -285,9 +283,27 @@ sub pkg_link {
 sub pkg_status_row {
   my( $cust_pkg, $title, $field, %opt ) = @_;
 
+  if ( $field and $cust_pkg->main_pkgnum ) {
+    # for supplemental packages, we mostly only show these if they're 
+    # different from the main package
+    my $main_pkg = $cust_pkg-> main_pkg;
+    if (    $main_pkg->get($field) ne $cust_pkg->get($field)
+        # with some exceptions
+        or  $field eq 'bill'
+        or  $field eq 'last_bill'
+        or  $field eq 'setup'
+        or  $field eq 'susp'
+        or  $field eq 'cancel'
+      ) {
+      # handle it normally
+    } else {
+      return '';
+    }
+  }
+
   my $color = $opt{'color'};
 
-  my $html = qq(<TR><TD WIDTH="<%$width%>" ALIGN="right">);
+  my $html = qq(<TR><TD WIDTH="$width" ALIGN="right">);
   $html   .= qq(<FONT COLOR="#$color"><B>) if length($color);
   $html   .= qq($title&nbsp;);
   $html   .= qq(</B></FONT>) if length($color);
@@ -338,7 +354,6 @@ sub pkg_status_row_changed {
                                      '',
                                      'size'    => '-1',
                                      'align'   => 'right',
-                                     'colspan' => $opt{'colspan'},
                                    );
   }
 
@@ -356,9 +371,7 @@ sub pkg_status_row_noauto {
   return '' unless $cust_main->payby =~ /^(CARD|CHEK)$/;
   my $what = lc(FS::payby->shortname($cust_main->payby));
 
-  pkg_status_row_colspan( $cust_pkg, emt("No automatic $what charge"), '',
-                          'colspan' => $opt{'colspan'},
-                        );
+  pkg_status_row_colspan( $cust_pkg, emt("No automatic $what charge"), '');
 }
 
 sub pkg_status_row_discount {
@@ -382,15 +395,24 @@ sub pkg_status_row_discount {
                   $cust_pkg_discount->pkgdiscountnum.
                 '">'.emt('remove discount').'</A>)</FONT>';
 
-    $html .= pkg_status_row_colspan( $cust_pkg, $label, '',
-                                     'colspan' => $opt{'colspan'},
-                                   );
+    $html .= pkg_status_row_colspan( $cust_pkg, $label, '', %opt );
 
   }
 
   $html;
 }
 
+sub pkg_reason_row {
+  my ($cust_pkg, $cpr, %opt) = @_;
+  return '' if $cust_pkg->main_pkgnum;
+
+  my $reasontext = '';
+  $reasontext = $cpr->reasontext . ' by ' . $cpr->otaker if $cpr;
+  pkg_status_row_colspan( $cust_pkg, $reasontext, '',
+    'align'=>'right', 'size'=>'-2', %opt
+  );
+}
+
 sub pkg_status_row_colspan {
   my($cust_pkg, $title, $addl, %opt) = @_;
 
index 942b42f..915be49 100644 (file)
@@ -34,7 +34,7 @@
   <A HREF="<% $p %>edit/cust_pay.cgi?payby=WEST;custnum=<% $custnum %>"><% mt('Enter Western Union payment') |h %></A>
 % } 
 
-<BR>
+<% $s ? '<BR>' : '' %>
 % $s=0;
 
 % if ( ( $payby{'CARD'} || $payby{'DCRD'} )
   <A HREF="<% $p %>edit/cust_pay.cgi?payby=MCRD;custnum=<% $custnum %>"><% mt('Post manual (offline/POS) credit card payment') |h %></A>
 % } 
 
-<BR>
+<% $s ? '<BR>' : '' %>
 
-%# credit link
+%# credit links
 
+% $s=0;
 % if ( $curuser->access_right('Post credit') ) { 
+  <% $s++ ? ' | ' : '' %>
   <& /elements/popup_link-cust_main.html,
                'label'       => emt('Enter credit'),
                'action'      => "${p}edit/cust_credit.cgi",
@@ -70,7 +72,9 @@
                'actionlabel' => emt('Enter credit'),
                'width'       => 616, #make room for reasons #540 default
   &>
-  |
+% }
+% if ( $curuser->access_right('Credit line items') ) { 
+  <% $s++ ? ' | ' : '' %>
   <& /elements/popup_link-cust_main.html,
                'label'       => emt('Credit line items'),
                #'action'      => "${p}search/cust_bill_pkg.cgi?nottax=1;type=select",
@@ -80,8 +84,8 @@
                'width'       => 968, #763,
                'height'      => 575,
   &>
-  <BR>
 % } 
+<% $s ? '<BR>' : '' %>
 
 %# refund links
 
 
 %#display payment history
 
-%my $money_char = $conf->config('money_char') || '$';
-%
-%sub balance_forward_row {
-%  my( $b, $date, $money_char ) = @_;
-%  ( my $balance_forward = $money_char. $b ) =~ s/^\$\-/-&nbsp;\$/;
-
-   <TR ID="balance_forward_row">
-     <TD CLASS="grid" BGCOLOR="#dddddd">
-       <% time2str($date_format, $date) %>
-     </TD>
-
-     <TD CLASS="grid" BGCOLOR="#dddddd">
-       <I><% mt("Starting balance on [_1]", time2str($date_format, $date) ) |h %></I>
-       (<A HREF="javascript:void(0);" onClick="show_history();"><% mt('show prior history') |h %></A>)
-     </TD>
-
-     <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
-     <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
-     <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
-     <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
-     <TD CLASS="grid" BGCOLOR="#dddddd" ALIGN="right"><I><% $balance_forward %></I></TD>
-
-   </TR>
-%}
-%
-%my $balance = 0;
 %my %target = ();
 %
-%my $years =  $conf->config('payment_history-years') || 2;
-%my $older_than = time - $years * 31556926; #60*60*24*365.2422
 %my $hidden = 0;
 %my $seen = 0;
 %my $old_history = 0;
 %my $lastdate = 0;
 %
-%foreach my $item ( sort { $a->{'date'} <=> $b->{'date'} } @history ) {
+%foreach my $item ( @history ) {
 %
 %  $lastdate = $item->{'date'};
 %
-%  my $display;
-%  if ( $item->{'date'} < $older_than ) {
+%  my $display = '';
+%  if ( $item->{'hide'} ) {
 %    $display = ' STYLE="display:none" ';
-%    $hidden = 1;
-%  } else {
-%
-%    $display = '';
-%
-%    if ( $hidden && ! $seen++ ) {
-%      balance_forward_row($balance, $item->{'date'}, $money_char);
-%    }
-%
 %  }
 %
 %  if ( $bgcolor eq $bgcolor1 ) {
 %
 %  my $target = exists($item->{'target'}) ? $item->{'target'} : '';
 %
-%  $balance += $item->{'charge'}  if exists $item->{'charge'};
-%  $balance -= $item->{'payment'} if exists $item->{'payment'};
-%  $balance -= $item->{'credit'}  if exists $item->{'credit'};
-%  $balance += $item->{'refund'}  if exists $item->{'refund'};
-%  $balance = sprintf("%.2f", $balance);
-%  $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
-%  ( my $showbalance = $money_char. $balance ) =~ s/^\$\-/-&nbsp;\$/;
-%
-%
-
+%  my $showbalance = $money_char . $item->{'balance'};
+%  $showbalance =~ s/^\$\-/-&nbsp;\$/;
 
   <TR <% $display ? $display.' ID="old_history'.$old_history++.'"'  : ''%>>
     <TD VALIGN="top" CLASS="grid" BGCOLOR="<% $bgcolor %>">
       <% $showbalance %>
     </TD>
   </TR>
-% } 
 
-%if ( scalar(@history) && $hidden && ! $seen++ ) {
-%  balance_forward_row($balance, $lastdate, $money_char);
-%}
+% if ( $item->{'balance_forward'} ) {
+<& .balance_forward_row, $item->{'balance'}, $item->{'date'} &>
+% } 
+%} # foreach $item
 
 </TABLE>
     </TD>
@@ -382,14 +341,37 @@ function show_history () {
 }
 
 </SCRIPT>
+<%def .balance_forward_row>
+%  my( $b, $date ) = @_;
+%  ( my $balance_forward = $money_char. $b ) =~ s/^\$\-/-&nbsp;\$/;
 
-<%init>
+   <TR ID="balance_forward_row">
+     <TD CLASS="grid" BGCOLOR="#dddddd">
+       <% time2str($date_format, $date) %>
+     </TD>
 
-my( $cust_main ) = @_;
-my $custnum = $cust_main->custnum;
+     <TD CLASS="grid" BGCOLOR="#dddddd">
+       <I><% mt("Starting balance on [_1]", time2str($date_format, $date) ) |h %></I>
+       (<A HREF="javascript:void(0);" onClick="show_history();"><% mt('show prior history') |h %></A>)
+     </TD>
+
+     <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+     <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+     <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+     <TD CLASS="grid" BGCOLOR="#dddddd"></TD>
+     <TD CLASS="grid" BGCOLOR="#dddddd" ALIGN="right"><I><% $balance_forward %></I></TD>
 
+   </TR>
+</%def>
+<%shared>
 my $conf = new FS::Conf;
 my $date_format = $conf->config('date_format') || '%m/%d/%Y';
+my $money_char = $conf->config('money_char') || '$';
+</%shared>
+<%init>
+
+my( $cust_main ) = @_;
+my $custnum = $cust_main->custnum;
 
 my $curuser = $FS::CurrentUser::CurrentUser;
 
@@ -497,6 +479,17 @@ foreach my $cust_pay_pending ($cust_main->cust_pay_pending_attempt) {
     #'target'  => $target, #XXX
   };
 }
+#declined batch payments
+foreach my $cust_pay_batch (
+  $cust_main->cust_pay_batch(hashref => {status => 'Declined'})
+) {
+  my $pay_batch = $cust_pay_batch->pay_batch;
+  push @history, {
+    'date'    => $pay_batch->upload,
+    'desc'    => include('payment_history/attempted_batch_payment.html', $cust_pay_batch, %opt),
+    'void_payment' => $cust_pay_batch->amount,
+  };
+}
 
 #credits (some false laziness w/payments)
 foreach my $cust_credit ($cust_main->cust_credit) {
@@ -518,6 +511,41 @@ foreach my $cust_refund ($cust_main->cust_refund) {
 
 }
 
+# sort in forward order first, and calculate running balances
+my $years =  $conf->config('payment_history-years') || 2;
+my $older_than = time - $years * 31556926; #60*60*24*365.2422
+my $balance = 0;
+
+@history = sort { $a->{date} <=> $b->{date} } @history;
+my $i = 0;
+my $balance_forward;
+foreach my $item (@history) {
+  $balance += $item->{'charge'}  if exists $item->{'charge'};
+  $balance -= $item->{'payment'} if exists $item->{'payment'};
+  $balance -= $item->{'credit'}  if exists $item->{'credit'};
+  $balance += $item->{'refund'}  if exists $item->{'refund'};
+  $balance = sprintf("%.2f", $balance);
+  $balance =~ s/^\-0\.00$/0.00/;
+  $item->{'balance'} = $balance;
+
+  if ( $item->{'date'} < $older_than ) {
+    $item->{'hide'} = 1;
+  } elsif ( $history[$i-1]->{'hide'} ) {
+    # this is the end of the hidden section
+    $history[$i-1]->{'balance_forward'} = 1;
+  }
+  $i++;
+}
+if ( @history and $history[-1]->{'hide'} ) {
+  # then everything is hidden
+  $history[-1]->{'balance_forward'} = 1;
+}
+
+# then sort in user-pref order
+if ( $curuser->option('history_order') eq 'newest' ) {
+  @history = sort { $b->{date} <=> $a->{date} } @history;
+} # else it's already oldest-first, and there are no other options yet
+
 sub translate_payby {
     my ($payby,$payinfo) = (shift,shift);
     my %payby = (
diff --git a/httemplate/view/cust_main/payment_history/attempted_batch_payment.html b/httemplate/view/cust_main/payment_history/attempted_batch_payment.html
new file mode 100644 (file)
index 0000000..95947f5
--- /dev/null
@@ -0,0 +1,13 @@
+<I><% mt('Payment attempt') |h %> <% $info |h %></I>
+<%init>
+
+my( $cust_pay_batch, %opt ) = @_;
+
+my ($payby,$payinfo) = translate_payinfo($cust_pay_batch);
+$payby = translate_payby($payby,$payinfo);
+my $info = $payby ? "($payby$payinfo)" : '';
+
+$info .= ': '. $cust_pay_batch->error_message
+  if length($cust_pay_batch->error_message);
+
+</%init>
index f7c685c..d735195 100644 (file)
@@ -52,26 +52,39 @@ function areyousure(href) {
 
 <% mt('Service #') |h %><B><% $svcnum %></B>
 % my $url = $opt{'edit_url'} || $p. 'edit/'. $opt{'table'}. '.cgi?';
-<& /view/elements/svc_edit_link.html, 'svc' => $svc_x, 'edit_url' => $url &>
+<& /view/elements/svc_edit_link.html, 'svc' => $svc_x, 'edit_url' => $url &>
 <BR>
 
 <% ntable("#cccccc") %><TR><TD><% ntable("#cccccc",2) %>
 
+% my @inventory_items = $svc_x->inventory_item;
 % foreach my $f ( @$fields ) {
 %
-%   my($field, $type, $value, $hack_strict_refs);
+%   my($field, $type, $value);
 %   if ( ref($f) ) {
 %     $field = $f->{'field'};
-%     $hack_strict_refs = \&{ $f->{'value'} } if $f->{'value'};
-%     $value = $f->{'value'} ? &$hack_strict_refs($svc_x) : $svc_x->$field;
 %     $type  = $f->{'type'} || 'text';
+%     if ( $f->{'value_callback'} ) {
+%       my $hack_strict_refs = \&{ $f->{'value_callback'} };
+%       $value = &$hack_strict_refs($svc_x);
+%     } else {
+%       $value = exists($f->{'value'}) ? $f->{'value'} : $svc_x->$field;
+%     }
 %   } else {
 %     $field = $f;
-%     $value = $svc_x->$field;
 %     $type = 'text';
+%     $value = $svc_x->$field;
 %   }
 %
 %   my $columndef = $part_svc->part_svc_column($field);
+%   if ( $columndef->columnflag =~ /^[MA]$/ && $columndef->columnvalue =~ /,/ )
+%   {
+%     # inventory-select field with multiple classes
+%     # show the class name to disambiguate
+%     my ($item) = grep { $_->svc_field eq $field } @inventory_items;
+%     my $class = qsearchs('inventory_class', { classnum => $item->classnum });
+%     $value .= ' <i>('. $class->classname . ')</i>' if $class;
+%   }
 %   unless ($columndef->columnflag eq 'F' && !length($columndef->columnvalue)) {
 
       <TR>
index d71c82f..38c6d09 100644 (file)
          )
 
 </%doc>
-<% $devices %>
+%if ( @devices || $num_part_device || $table eq 'dsl_device' ) {
+%  my $svcnum = $svc_x->svcnum;
+
+   Devices
+   (<A HREF="<%$p%>edit/<%$table%>.html?svcnum=<%$svcnum%>">Add device</A>)
+   <BR>
+
+%  if ( @devices ) {
+
+     <SCRIPT>
+       function areyousure(href) {
+        if (confirm("Are you sure you want to delete this device?") == true)
+          window.location.href = href;
+       }
+     </SCRIPT>
+
+     <& /elements/table-grid.html &>
+       <TR>
+%        if ( $table eq 'phone_device' ) {
+           <TH CLASS="grid" BGCOLOR="#cccccc">Type</TH>
+%        }
+         <TH CLASS="grid" BGCOLOR="#cccccc">MAC Addr</TH>
+         <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
+         <TH CLASS="grid" BGCOLOR="#cccccc"></TH>
+       </TR>
+
+%      my $bgcolor1 = '#eeeeee';
+%      my $bgcolor2 = '#ffffff';
+%      my $bgcolor = '';
+%
+%      foreach my $device ( @devices ) {
+%
+%        if ( $bgcolor eq $bgcolor1 ) {
+%          $bgcolor = $bgcolor2;
+%        } else {
+%          $bgcolor = $bgcolor1;
+%        }
+%
+%        my $td = qq(<TD CLASS="grid" BGCOLOR="$bgcolor">);
+%
+%        my $devicenum = $device->devicenum;
+%        my $export_links = '';
+%        $export_links = join( '<BR>', @{ $device->export_links } )
+%          if $device->can('export_links');
+
+        <TR>
+%         if ( $table eq 'phone_device' ) { #$devices->can('part_device')
+            <% $td %><% $device->part_device->devicename |h %></TD>
+%         }
+          <% $td %><% $device->mac_addr %></TD>
+          <% $td %><% $export_links %></TD>
+          <% $td %>(
+%           unless ( $opt{'no_edit'} ) {
+              <A HREF="<%$p%>edit/<%$table%>.html?<%$devicenum%>">edit</A> |
+%           }
+            <A HREF="javascript:areyousure('<%$p%>misc/delete-<%$table%>.html?<%$devicenum%>')">delete</A>
+          )</TD>
+        </TR>
+%     }
+      </TABLE>
+      <BR>
+
+%  }
+   <BR>
+%}
 <%init>
 
-  my %opt = @_;
-  my $table = $opt{'table'}; #part_device, dsl_device
-  my $svc_x = $opt{'svc_x'};
-
-  my $devices = '';
-
-  my $num_part_device = 0;
-  if ( $table eq 'phone_device' )  {
-    my $sth = dbh->prepare("SELECT COUNT(*) FROM part_device")
-                              #WHERE disabled = '' OR disabled IS NULL;");
-      or die dbh->errstr;
-    $sth->execute or die $sth->errstr;
-    $num_part_device = $sth->fetchrow_arrayref->[0];
+my %opt = @_;
+my $table = $opt{'table'}; #part_device, dsl_device
+my $svc_x = $opt{'svc_x'};
+
+my $num_part_device = 0;
+if ( $table eq 'phone_device' ) {
+  my $sth = dbh->prepare("SELECT COUNT(*) FROM part_device")
+                            #WHERE disabled = '' OR disabled IS NULL;");
+    or die dbh->errstr;
+  $sth->execute or die $sth->errstr;
+  $num_part_device = $sth->fetchrow_arrayref->[0];
 }
 
-  my @devices = $svc_x->$table();
-
-  #should move the below to proper mason code above instead of making $devices
-  if ( @devices || $num_part_device || $table eq 'dsl_device' ) {
-    my $svcnum = $svc_x->svcnum;
-    $devices .=
-      qq[Devices (<A HREF="${p}edit/$table.html?svcnum=$svcnum">Add device</A>)<BR>];
-    if ( @devices ) {
-
-      $devices .= qq!
-        <SCRIPT>
-          function areyousure(href) {
-           if (confirm("Are you sure you want to delete this device?") == true)
-             window.location.href = href;
-          }
-        </SCRIPT>
-      !;
-
-
-      $devices .= 
-        include('/elements/table-grid.html').
-          '<TR>';
-
-      $devices .=
-            '<TH CLASS="grid" BGCOLOR="#cccccc">Type</TH>'
-        if $table eq 'phone_device';
-
-      $devices .=
-            '<TH CLASS="grid" BGCOLOR="#cccccc">MAC Addr</TH>'.
-            '<TH CLASS="grid" BGCOLOR="#cccccc"></TH>'.
-            '<TH CLASS="grid" BGCOLOR="#cccccc"></TH>'.
-          '</TR>';
-      my $bgcolor1 = '#eeeeee';
-      my $bgcolor2 = '#ffffff';
-      my $bgcolor = '';
-
-      foreach my $device ( @devices ) {
-
-        if ( $bgcolor eq $bgcolor1 ) {
-          $bgcolor = $bgcolor2;
-        } else {
-          $bgcolor = $bgcolor1;
-        }
-        my $td = qq(<TD CLASS="grid" BGCOLOR="$bgcolor">);
-
-        my $devicenum = $device->devicenum;
-        my $export_links = join( '<BR>', @{ $device->export_links } )
-          if $device->can('export_links');
-
-        $devices .= '<TR>';
-        $devices .= $td. $device->part_device->devicename. '</TD>'
-          if $table eq 'phone_device'; #$devices->can('part_device');
-
-        $devices .=   $td. $device->mac_addr. '</TD>'.
-                      $td. $export_links. '</TD>'.
-                      "$td( ";
-
-        $devices .= qq(<A HREF="${p}edit/$table.html?$devicenum">edit</A> | )
-          unless $opt{'no_edit'};
-
-        $devices .= qq(<A HREF="javascript:areyousure('${p}misc/delete-$table.html?$devicenum')">delete</A>).
-                      ' )</TD>'.
-                    '</TR>';
-      }
-      $devices .= '</TABLE><BR>';
-    }
-    $devices .= '<BR>';
-  }
+my @devices = $svc_x->$table();
 
 </%init>
index d65db0a..5438ed2 100644 (file)
@@ -7,8 +7,12 @@ function areyousure_delete() {
     window.location.href = '<% $cancel_url %>';
 }
 </SCRIPT>
-<A HREF="<% $edit_url %>"><% mt("Edit this [_1]", $label) |h %></A> | 
-<A HREF="javascript:areyousure_delete()"><% mt('Unprovision this Service') |h %></A>
+%   if ( $curuser->access_right('Provision customer service') ) {
+| <A HREF="<% $edit_url %>"><% mt("Edit this [_1]", $label) |h %></A>
+%   }
+%   if ( $curuser->access_right('Unprovision customer service') ) {
+| <A HREF="javascript:areyousure_delete()"><% mt('Unprovision this Service') |h %></A>
+%   }
 % }
 <%init>
 my %opt = @_;
@@ -20,4 +24,5 @@ my $cancel_url = $p . 'misc/unprovision.cgi?' . $svc_x->svcnum;
 my $cust_svc = $svc_x->cust_svc; # always exists
 my $cancel_date = $cust_svc->pkg_cancel_date;
 my ($label) = $cust_svc->label;
+my $curuser = $FS::CurrentUser::CurrentUser;
 </%init>
index d96bb27..4ce869e 100644 (file)
@@ -7,7 +7,15 @@
 %     foreach my $key ( sort {$a cmp $b} keys %$hashref ) {
         <TR>
           <TD ALIGN="right"><% $key |h %></TD>
-          <TD BGCOLOR="#ffffff"><% $hashref->{$key} |h %></TD>
+          <TD BGCOLOR="#ffffff">
+%           if ( ref($hashref->{$key}) eq 'ARRAY' ) {
+%             foreach (@{ $hashref->{$key} }) {
+                <% $_ |h %><BR>
+%             }
+%           } else {
+              <% $hashref->{$key} |h %>
+%           }
+          </TD>
         </TR>
 %     }
 
diff --git a/httemplate/view/quotation-pdf.cgi b/httemplate/view/quotation-pdf.cgi
new file mode 100755 (executable)
index 0000000..7f62ce1
--- /dev/null
@@ -0,0 +1,29 @@
+<% $content %>\
+<%init>
+
+#false laziness w/elements/cust_bill-typeset
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Generate quotation'); #View quotations ?
+
+my $quotationnum = $cgi->param('quotationnum');
+
+my $conf = new FS::Conf;
+
+my $quotation = qsearchs({
+  'select'    => 'quotation.*',
+  'table'     => 'quotation',
+  #'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+  'hashref'   => { 'quotationnum' => $quotationnum },
+  #'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+});
+die "Quotation #$quotationnum not found!" unless $quotation;
+
+my $content = $quotation->print_pdf(); #\%opt);
+
+http_header('Content-Type' => 'application/pdf');
+http_header('Content-Disposition' => "filename=$quotationnum.pdf" );
+http_header('Content-Length' => length($content) );
+http_header('Cache-control' => 'max-age=60' );
+
+</%init>
index 1995913..858ccbe 100755 (executable)
@@ -22,6 +22,7 @@
 
 % } 
 
+
 <& svc_acct/radius_usage.html,
               'svc_acct' => $svc_acct,
               'part_svc' => $part_svc,
@@ -29,6 +30,7 @@
               %gopt,
 &>
 
+
 <& svc_acct/change_svc_form.html,
               'part_svc' => \@part_svc,
               'svcnum'   => $svcnum,
 &>
 
 <% mt('Service #') |h %><B><% $svcnum %></B>
-|
 <& /view/elements/svc_edit_link.html, 'svc' => $svc_acct &>
 <& svc_acct/change_svc.html,
               'part_svc' => \@part_svc,
               %gopt,
 &>
 
+</FORM>
+
+
 <& svc_acct/basics.html,
               'svc_acct' => $svc_acct,
               'part_svc' => $part_svc,
@@ -90,8 +94,12 @@ die "access denied"
 my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
                 ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
                 ' LEFT JOIN cust_main USING ( custnum ) ';
-
-my($query) = $cgi->keywords;
+my $query;
+if ( $cgi->keywords ) {
+  ($query) = $cgi->keywords;
+} else {
+  $query = $cgi->param('svcnum');
+}
 $query =~ /^(\d+)$/;
 my $svcnum = $1;
 my $svc_acct = qsearchs({
index 2d9953f..04e7bcf 100644 (file)
@@ -20,7 +20,7 @@
 % if ( $password =~ /^\*\w+\* (.*)$/ ) {
 %   $password = $1;
 %   $show_pw .= '<I>('. mt('login disabled') .')</I> ';
-% } 
+% }
 % if ( ! $password
 %      && $svc_acct->_password_encryption ne 'plain'
 %      && $svc_acct->_password
 % {
 %   $show_pw .= '<I>('. uc($svc_acct->_password_encryption). ' '.mt('encrypted').')</I>';
 % } elsif ( $conf->exists('showpasswords') ) { 
-%   $show_pw .= '<PRE>'. encode_entities($password). '</PRE>';
+%   $show_pw .= '<SPAN >'. encode_entities($password). '</PRE>';
 % } else { 
+%   $password = '';
 %   $show_pw .= '<I>('. mt('hidden') .')</I>';
-% } 
-% $password = ''; 
-<& /view/elements/tr.html, label=>mt('Password'), value=>$show_pw &>
-
+% }
+<TR>
+  <TD ALIGN="right"><% mt('Password') %></TD>
+  <TD STYLE="background-color: #ffffff; white-space: nowrap">
+  <% $show_pw %>
+% my $curuser = $FS::CurrentUser::CurrentUser;
+% if ( $curuser->access_right('Provision customer service') or
+%     ($curuser->access_right('Edit password') and
+%      ! $part_svc->restrict_edit_password) )
+% {
+  <& /elements/change_password.html,
+      'svc_acct'    => $svc_acct,
+      'curr_value'  => $password,
+  &>
+% }
+  </TD>
+</TR>
 
 % if ( $conf->exists('security_phrase') ) {
   <& /view/elements/tr.html, label=>mt('Security phrase'), value=>$svc_acct->sec_phrase &>
index 75e673c..7d6520e 100644 (file)
@@ -26,23 +26,31 @@ $labels{'coordinates'} = 'Latitude/Longitude';
 
 my @fields = (
   'description',
-  { field => 'routernum', value => \&router },
+  { field => 'routernum', value_callback => \&router },
   'speed_down',
   'speed_up',
-  { field => 'ip_addr', value => \&ip_addr },
-  { field => 'sectornum', value => \&sectornum },
-  { field => 'mac_addr', value => \&mac_addr },
+  { field => 'ip_addr', value_callback => \&ip_addr },
+  { field => 'sectornum', value_callback => \&sectornum },
+  { field => 'mac_addr', value_callback => \&mac_addr },
   #'latitude',
   #'longitude',
-  { field => 'coordinates', value => \&coordinates },
+  { field => 'coordinates', value_callback => \&coordinates },
   'altitude',
+
+  'radio_serialnum',
+  'radio_location',
+  'poe_location',
+  'rssi',
+  'suid',
+  { field => 'shared_svcnum', value_callback=> \&shared_svcnum, }, #value_callback => 
+
   'vlan_profile',
   'authkey',
   'plan_id',
 );
 
 push @fields,
-  { field => 'usergroup', value => \&usergroup }
+  { field => 'usergroup', value_callback => \&usergroup }
   if $conf->exists('svc_broadband-radius');
 
 sub router {
@@ -112,9 +120,36 @@ sub coordinates {
     );
 }
 
+sub shared_svcnum {
+  my $svc_broadband = shift;
+  return '' unless $svc_broadband->shared_svcnum;
+
+  my $shared_svc_broadband =
+    qsearchs('svc_broadband', { 'svcnum' => $svc_broadband->shared_svcnum,
+                              }
+                              #agent virt?
+            )
+      or return '';
+  my $shared_cust_pkg = $shared_svc_broadband->cust_svc->cust_pkg;
+
+  $shared_svc_broadband->label.
+    ( $shared_cust_pkg
+         ? ' ('. $shared_cust_pkg->cust_main->name. ')'
+         : ''
+    );
+}
+
 sub svc_callback {
   # trying to move to the callback style
   my ($cgi, $svc_x, $part_svc, $cust_pkg, $fields, $opt) = @_;
+
+  if (    $part_svc->part_svc_column('latitude')->columnflag eq 'F' 
+       && $part_svc->part_svc_column('longitude')->columnflag eq 'F' 
+     )
+  {
+    @$fields = grep { !ref($_) || $_->{field} ne 'coordinates' } @$fields;
+  }
+
   # again, we assume at most one of these exports per part_svc
   my ($nas_export) = $part_svc->part_export('broadband_nas');
   if ( $nas_export ) {
index 0cd66b4..964b808 100644 (file)
@@ -17,7 +17,7 @@ my %labels = map { $_ =>  ( ref($fields->{$_})
 
 my @fields = (
   { field=>'privatekey',
-    value=> sub {
+    value_callback=> sub {
       my $svc_cert = shift;
       if ( $svc_cert->privatekey && $svc_cert->check_privatekey ) {
         '<FONT COLOR="#33ff33">Verification OK</FONT>';
@@ -31,7 +31,7 @@ my @fields = (
   qw( common_name organization organization_unit city state country cert_contact
     ),
   { 'field'=>'csr',
-    'value'=> sub {
+    'value_callback'=> sub {
       my $svc_cert = shift;
       if ( $svc_cert->csr ) {
 
@@ -67,7 +67,7 @@ my @fields = (
     },
   },
   { 'field'=>'certificate',
-    'value'=> sub {
+    'value_callback'=> sub {
       my $svc_cert = shift;
       if ( $svc_cert->certificate ) {
 
@@ -137,7 +137,7 @@ my @fields = (
     },
   },
   { 'field'=>'cacert',
-    'value'=> sub {
+    'value_callback'=> sub {
       my $svc_cert = shift;
       if ( $svc_cert->cacert ) {
 
index 7f5e889..eef1c11 100644 (file)
@@ -13,17 +13,20 @@ my %labels = map { $_ =>  ( ref($fields->{$_})
                              : $fields->{$_}
                          );
                  } keys %$fields;
+
+$labels{'display_hw_addr'} = 'Hardware address';
+
 my $model =  { field => 'typenum',
                type  => 'text',
-               value => sub { $_[0]->hardware_type->description }
+               value_callback => sub { $_[0]->hardware_type->description }
              };
 my $status = { field => 'statusnum',
                type  => 'text',
-               value => sub { $_[0]->status_label }
+               value_callback => sub { $_[0]->status_label }
              };
 my $note =   { field => 'note',
                type  => 'text',
-               value => sub { encode_entities($_[0]->note) }
+               value_callback => sub { encode_entities($_[0]->note) }
              };
 
 my @fields = (
index 323be63..ed95c4c 100644 (file)
@@ -16,9 +16,20 @@ my %labels = map { $_ =>  ( ref($fields->{$_})
                          );
                  } keys %$fields;
 
-my @fields = qw( countrycode phonenum );
+my @fields = qw( countrycode phonenum sim_imsi );
 push @fields, 'domain' if $conf->exists('svc_phone-domain');
-push @fields, qw( pbx_title sip_password pin phone_name forwarddst email );
+push @fields, qw( pbx_title );
+
+if ( $conf->exists('showpasswords') ) {
+  push @fields, qw( sip_password );
+} else {
+  push @fields, { 'field' => 'sip_password', #'_HIDDEN_sip_password',
+                  'type'  => 'fixed',
+                  'value' => '<I>('. mt('hidden') .')</I>',
+                };
+}
+
+push @fields, qw( pin phone_name forwarddst email );
 
 if ( $conf->exists('svc_phone-lnp') ) {
 push @fields, 'lnp_status',
index 315d6b2..ace0d49 100644 (file)
@@ -160,3 +160,8 @@ share/html/Ticket/Elements/ShowDates
 share/html/Elements/CustomerFields
 share/html/Search/Elements/ConditionRow # bugfix for select options list
 share/html/Search/Elements/PickBasics
+
+#avoid cloning TimeWorked and related fields
+lib/RT/CustomField.pm
+share/html/Admin/CustomFields/Modify.html
+share/html/Ticket/Create.html
index 2a7a2e3..1e6607e 100755 (executable)
@@ -871,21 +871,25 @@ sub SetFrom {
     my $self = shift;
     my %args = @_;
 
+    my $from = $args{From};
+
     if ( RT->Config->Get('UseFriendlyFromLine') ) {
         my $friendly_name = $self->GetFriendlyName(%args);
-        $self->SetHeader(
-            'From',
+        $from = 
             sprintf(
                 RT->Config->Get('FriendlyFromLineFormat'),
                 $self->MIMEEncodeString(
                     $friendly_name, RT->Config->Get('EmailOutputEncoding')
                 ),
                 $args{From}
-            ),
-        );
-    } else {
-        $self->SetHeader( 'From', $args{From} );
+            );
     }
+
+    $self->SetHeader( 'From', $from );
+
+    #also set Sender:, otherwise MTAs add a nonsensical value like rt@machine,
+    #and then Outlook prepends "rt@machine on behalf of" to the From: header
+    $self->SetHeader( 'Sender', $from );
 }
 
 =head2 GetFriendlyName
index 7ba24b8..8d16c1f 100644 (file)
@@ -410,6 +410,10 @@ sub Create {
             $self->SetUILocation( $args{'UILocation'} );
         }
 
+        if ( exists $args{'NoClone'} ) {
+            $self->SetNoClone( $args{'NoClone'} );
+        }
+
         return ($rv, $msg) unless exists $args{'Queue'};
 
         # Compat code -- create a new ObjectCustomField mapping
@@ -1822,9 +1826,20 @@ sub SetUILocation {
     }
 }
 
+sub NoClone {
+    my $self = shift;
+    $self->FirstAttribute('NoClone') ? 1 : '';
+}
 
-
-
+sub SetNoClone {
+    my $self = shift;
+    my $value = shift;
+    if ( $value ) {
+        return $self->SetAttribute( Name => 'NoClone', Content => 1 );
+    } else {
+        return $self->DeleteAttribute('NoClone');
+    }
+}
 
 
 =head2 id
index 4ed86b6..358dcfd 100644 (file)
 </td></tr>
 
 <tr><td class="label">&nbsp;</td><td>
+<input type="checkbox" class="checkbox" name="YesClone" value="1" <% $YesCloneChecked |n%> />
+<&|/l&>Copy this field to new tickets</&>
+</td></tr>
+
+<tr><td class="label">&nbsp;</td><td>
 <input type="hidden" class="hidden" name="SetEnabled" value="1" />
 <input type="checkbox" class="checkbox" name="Enabled" value="1" <% $EnabledChecked |n%> />
 <&|/l&>Enabled (Unchecking this box disables this custom field)</&>
@@ -187,6 +192,7 @@ else {
             IncludeContentForValue => $IncludeContentForValue,
             BasedOn       => $BasedOn,
             Disabled      => !$Enabled,
+            NoClone       => !$YesClone,
         );
         if (!$val) {
             push @results, loc("Could not create CustomField: [_1]", $msg);
@@ -207,10 +213,12 @@ else {
 if ( $ARGS{'Update'} && $id ne 'new' ) {
     #we're asking about enabled on the web page but really care about disabled.
     $ARGS{'Disabled'} = $Enabled? 0 : 1;
+    #  likewise
+    $ARGS{'NoClone'} = $YesClone ? 0 : 1;
    
     $ARGS{'Required'} ||= 0;
 
-    my @attribs = qw(Disabled Required Pattern Name TypeComposite LookupType Description LinkValueTo IncludeContentForValue);
+    my @attribs = qw(Disabled Required Pattern Name TypeComposite LookupType Description LinkValueTo IncludeContentForValue NoClone);
     push @results, UpdateRecordObject(
         AttributesRef => \@attribs,
         Object        => $CustomFieldObj,
@@ -313,6 +321,10 @@ $EnabledChecked = '' if $CustomFieldObj->Disabled;
 my $RequiredChecked = '';
 $RequiredChecked = qq[checked="checked"] if $CustomFieldObj->Required;
 
+my $YesCloneChecked = qq[checked="checked"];
+$YesCloneChecked = '' if $CustomFieldObj->NoClone;
+
+
 my @CFvalidations = (
     '(?#Mandatory).',
     '(?#Digits)^[\d.]+$',
@@ -339,4 +351,5 @@ $LinkValueTo => undef
 $IncludeContentForValue => undef
 $BasedOn => undef
 $UILocation => undef
+$YesClone => undef
 </%ARGS>
index 0419126..8c6a58a 100755 (executable)
@@ -293,8 +293,8 @@ if ($CloneTicket) {
     };
 
     $clone->{$_} = $CloneTicketObj->$_()
-        for qw/Owner Subject FinalPriority TimeEstimated TimeWorked
-        Status TimeLeft/;
+        for qw/Owner Subject FinalPriority Status/;
+        # not TimeWorked, TimeEstimated, or TimeLeft
 
     $clone->{$_} = $CloneTicketObj->$_->AsString
         for grep { $CloneTicketObj->$_->Unix }
@@ -330,6 +330,7 @@ if ($CloneTicket) {
 
     my $cfs = $CloneTicketObj->QueueObj->TicketCustomFields();
     while ( my $cf = $cfs->Next ) {
+        next if $cf->FirstAttribute('NoClone');
         my $cf_id     = $cf->id;
         my $cf_values = $CloneTicketObj->CustomFieldValues( $cf->id );
         my @cf_values;