merge NG auth, RT#21563
authorIvan Kohler <ivan@freeside.biz>
Fri, 10 May 2013 19:55:52 +0000 (12:55 -0700)
committerIvan Kohler <ivan@freeside.biz>
Fri, 10 May 2013 19:55:52 +0000 (12:55 -0700)
191 files changed:
FS/FS.pm
FS/FS/AccessRight.pm
FS/FS/ClientAPI/MyAccount.pm
FS/FS/ClientAPI/Signup.pm
FS/FS/Conf.pm
FS/FS/Mason.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/agent.pm
FS/FS/cdr/gsm_tap3_12.pm
FS/FS/cdr/huawei_softx3000.pm [new file with mode: 0644]
FS/FS/contact_Mixin.pm [new file with mode: 0644]
FS/FS/cust_bill.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_location.pm
FS/FS/cust_main.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_main/Billing_Realtime.pm
FS/FS/cust_main/Packages.pm
FS/FS/cust_main/Search.pm
FS/FS/cust_main_county.pm
FS/FS/cust_pay.pm
FS/FS/cust_pkg.pm
FS/FS/cust_svc.pm
FS/FS/cust_tax_location.pm
FS/FS/export_svc.pm
FS/FS/part_event/Action/fee.pm
FS/FS/part_event/Condition/cust_bill_owed_percent.pm [new file with mode: 0644]
FS/FS/part_event/Condition/inactive_age.pm [new file with mode: 0644]
FS/FS/part_event/Condition/once_perinv.pm
FS/FS/part_export.pm
FS/FS/part_export/http_status.pm
FS/FS/part_export/huawei_hlr.pm
FS/FS/part_export/shellcommands.pm
FS/FS/part_export/test.pm [new file with mode: 0644]
FS/FS/part_pkg.pm
FS/FS/part_pkg/prorate_Mixin.pm
FS/FS/part_pkg/sqlradacct_daily.pm
FS/FS/part_pkg/voip_cdr.pm
FS/FS/part_pkg/voip_inbound.pm
FS/FS/part_pkg_msgcat.pm [new file with mode: 0644]
FS/FS/part_pkg_taxrate.pm
FS/FS/pay_batch/BoM.pm
FS/FS/pay_batch/nacha.pm
FS/FS/payby.pm
FS/FS/payinfo_Mixin.pm
FS/FS/payinfo_transaction_Mixin.pm
FS/FS/payment_gateway.pm
FS/FS/reason.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/FS/tax_class.pm
FS/FS/tax_rate.pm
FS/MANIFEST
FS/bin/freeside-cdr-sftp_and_import
FS/bin/freeside-queued
FS/bin/freeside-upgrade
FS/t/contact_Mixin.t [new file with mode: 0644]
FS/t/part_pkg_msgcat.t [new file with mode: 0644]
README
bin/3add [new file with mode: 0755]
bin/3commit [new file with mode: 0755]
bin/3diff [new file with mode: 0755]
bin/cch.finish_failed [new file with mode: 0644]
bin/cch.redelete [new file with mode: 0644]
conf/invoice_html
conf/invoice_latex
fs_selfservice/FS-SelfService/SelfService.pm
fs_selfservice/FS-SelfService/cgi/make_thirdparty_payment.html
fs_selfservice/FS-SelfService/cgi/myaccount.html
fs_selfservice/FS-SelfService/cgi/myaccount_menu.html
fs_selfservice/FS-SelfService/cgi/selfservice.cgi
fs_selfservice/FS-SelfService/cgi/signup.cgi
fs_selfservice/FS-SelfService/cgi/signup.html
fs_selfservice/FS-SelfService/cgi/small_custview.html
fs_selfservice/FS-SelfService/cgi/verify.cgi
htetc/freeside-rt.conf
httemplate/edit/agent_payment_gateway.html
httemplate/edit/cust_location.cgi
httemplate/edit/part_export.cgi
httemplate/edit/part_pkg.cgi
httemplate/edit/payment_gateway.html
httemplate/edit/process/change-cust_pkg.html
httemplate/edit/process/cust_location.cgi
httemplate/edit/process/cust_main.cgi
httemplate/edit/process/detach-cust_pkg.html [new file with mode: 0644]
httemplate/edit/process/part_export.cgi
httemplate/edit/process/part_pkg.cgi
httemplate/edit/process/payment_gateway.html
httemplate/edit/process/quick-cust_pkg.cgi
httemplate/edit/process/svc_phone.html
httemplate/edit/svc_broadband.cgi
httemplate/elements/auto-table.html
httemplate/elements/contact.html
httemplate/elements/dashboard-toplist.html
httemplate/elements/menu.html
httemplate/elements/search-svc_broadband.html [new file with mode: 0644]
httemplate/elements/select-tiered.html
httemplate/elements/selectlayers.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-voip_class.html
httemplate/misc/areacodes.cgi
httemplate/misc/batch-cust_pay.html
httemplate/misc/cancel-unaudited.cgi
httemplate/misc/change_pkg_contact.html [new file with mode: 0755]
httemplate/misc/choose_tax_location.html
httemplate/misc/cust-part_pkg.cgi
httemplate/misc/cust_main-merge.html
httemplate/misc/delete-note.html [new file with mode: 0644]
httemplate/misc/detach_pkg.html [new file with mode: 0755]
httemplate/misc/exchanges.cgi
httemplate/misc/location.cgi
httemplate/misc/macinventory.cgi
httemplate/misc/maestro-customer_status.html
httemplate/misc/merge_cust.html
httemplate/misc/order_pkg.html
httemplate/misc/part_svc-columns.cgi
httemplate/misc/phonenums.cgi
httemplate/misc/process/change_pkg_contact.html [new file with mode: 0644]
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
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/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/agent_commission.html
httemplate/search/cust_bill.html
httemplate/search/cust_bill_pay.html
httemplate/search/cust_bill_pkg.cgi
httemplate/search/cust_bill_pkg_referral.html
httemplate/search/cust_credit.html
httemplate/search/cust_credit_refund.html
httemplate/search/cust_main.cgi
httemplate/search/cust_pay_pending.html
httemplate/search/cust_svc.html
httemplate/search/customer_accounting_summary.html
httemplate/search/elements/cust_main_dayranges.html
httemplate/search/elements/cust_pay_or_refund.html
httemplate/search/elements/search-xls.html
httemplate/search/elements/search.html
httemplate/search/employee_audit.html
httemplate/search/h_cust_pay.html
httemplate/search/part_pkg.html
httemplate/search/prepaid_income.html
httemplate/search/report_cust_bill.html
httemplate/search/report_employee_audit.html
httemplate/search/report_employee_commission.html
httemplate/search/report_receivables.html
httemplate/search/report_tax.cgi
httemplate/search/report_tax.html
httemplate/search/unapplied_cust_pay.html
httemplate/search/unearned_detail.html
httemplate/view/cust_main.cgi
httemplate/view/cust_main/change_history.html
httemplate/view/cust_main/locations.html
httemplate/view/cust_main/notes.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/elements/svc_Common.html
httemplate/view/svc_Common.html
httemplate/view/svc_acct.cgi
httemplate/view/svc_broadband.cgi

index 741d815..042c756 100644 (file)
--- a/FS/FS.pm
+++ b/FS/FS.pm
@@ -3,7 +3,7 @@ package FS;
 use strict;
 use vars qw($VERSION);
 
-$VERSION = '3.0';
+$VERSION = '4.0git';
 
 #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
@@ -233,6 +233,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 a60d033..373617e 100644 (file)
@@ -132,6 +132,7 @@ tie my %rights, 'Tie::IxHash',
     'Order customer package',
     'One-time charge',
     'Change customer package',
+    'Detach customer package',
     'Bulk change customer packages',
     'Edit customer package dates',
     'Discount customer package', #NEW
@@ -305,6 +306,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 },
   ],
index 08e506c..01e0ebc 100644 (file)
@@ -50,7 +50,7 @@ $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
@@ -636,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 ),
@@ -1584,7 +1585,7 @@ 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 }
@@ -1698,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'),
             );
index 1dbb20b..57091c4 100644 (file)
@@ -98,7 +98,7 @@ sub signup_info {
 
     my @signup_bools = qw( no_company recommend_daytime recommend_email );
 
-    my @signup_server_scalars = qw( default_pkgpart default_svcpart );
+    my @signup_server_scalars = qw( default_pkgpart default_svcpart default_domsvc );
 
     my @selfservice_textareas = qw( head body_header body_footer );
 
@@ -670,7 +670,7 @@ sub new_customer {
     my $svc = new FS::svc_acct {
       'svcpart'   => $svcpart,
       map { $_ => $packet->{$_} }
-        qw( username _password sec_phrase popnum ),
+        qw( username _password sec_phrase popnum domsvc ),
     };
 
     my @acct_snarf;
@@ -946,15 +946,27 @@ sub capture_payment {
   }
 
   my $cust_main = $cust_pay_pending->cust_main;
-  my $bill_error =
-    $cust_main->realtime_botpp_capture( $cust_pay_pending, 
-      %{$packet->{data}},
-      apply => 1,
-  );
+  if ( $packet->{cancel} ) {
+    # the user has chosen not to make this payment
+    # (probably should be a separate API call, but I don't want to duplicate
+    # all of the above...which should eventually go away)
+    my $error = $cust_pay_pending->delete;
+    # don't show any errors related to this; they're not meaningful
+    warn "error canceling pending payment $paypendingnum: $error\n" if $error;
+    return { 'error'      => '_cancel',
+             'session_id' => $cust_pay_pending->session_id };
+  } else {
+    # create the payment
+    my $bill_error =
+      $cust_main->realtime_botpp_capture( $cust_pay_pending, 
+        %{$packet->{data}},
+        apply => 1,
+    );
 
-  return { 'error'      => ( $bill_error->{bill_error} ? '_decline' : '' ),
-           %$bill_error,
-         };
+    return { 'error'      => ( $bill_error->{bill_error} ? '_decline' : '' ),
+             %$bill_error,
+           };
+  }
 
 }
 
index 831ffe2..3c44520 100644 (file)
@@ -1038,6 +1038,9 @@ sub reason_type_options {
     'select_hash' => [
                        '%b %o, %Y' => 'Mon DDth, YYYY',
                        '%e %b %Y'  => 'DD Mon YYYY',
+                       '%m/%d/%Y'  => 'MM/DD/YYYY',
+                       '%d/%m/%Y'  => 'DD/MM/YYYY',
+                      '%Y/%m/%d'  => 'YYYY/MM/DD',
                      ],
   },
 
@@ -2098,7 +2101,7 @@ and customer address. Include units.',
     'section'     => 'self-service',
     'description' => 'Acceptable payment types for the signup server',
     'type'        => 'selectmultiple',
-    'select_enum' => [ qw(CARD DCRD CHEK DCHK LECB PREPAY BILL COMP) ],
+    'select_enum' => [ qw(CARD DCRD CHEK DCHK LECB PREPAY PPAL BILL COMP) ],
   },
 
   {
@@ -2159,11 +2162,18 @@ and customer address. Include units.',
   {
     'key'         => 'signup_server-default_svcpart',
     'section'     => 'self-service',
-    'description' => 'Default service definition for the signup server - only necessary for services that trigger special provisioning widgets (such as DID provisioning).',
+    'description' => 'Default service definition for the signup server - only necessary for services that trigger special provisioning widgets (such as DID provisioning or domain selection).',
     'type'        => 'select-part_svc',
   },
 
   {
+    'key'         => 'signup_server-default_domsvc',
+    'section'     => 'self-service',
+    'description' => 'If specified, the default domain svcpart for signup (useful when domain is set to selectable choice).',
+    'type'        => 'text',
+  },
+
+  {
     'key'         => 'signup_server-mac_addr_svcparts',
     'section'     => 'self-service',
     'description' => 'Service definitions which can receive mac addresses (current mapped to username for svc_acct).',
@@ -2472,7 +2482,7 @@ and customer address. Include units.',
     'section'     => 'billing',
     'description' => 'Available payment types.',
     'type'        => 'selectmultiple',
-    'select_enum' => [ qw(CARD DCRD CHEK DCHK LECB BILL CASH WEST MCRD COMP) ],
+    'select_enum' => [ qw(CARD DCRD CHEK DCHK LECB BILL CASH WEST MCRD PPAL COMP) ],
   },
 
   {
@@ -2480,7 +2490,7 @@ and customer address. Include units.',
     'section'     => 'UI',
     'description' => 'Default payment type.  HIDE disables display of billing information and sets customers to BILL.',
     'type'        => 'select',
-    'select_enum' => [ '', qw(CARD DCRD CHEK DCHK LECB BILL CASH WEST MCRD COMP HIDE) ],
+    'select_enum' => [ '', qw(CARD DCRD CHEK DCHK LECB BILL CASH WEST MCRD PPAL COMP HIDE) ],
   },
 
   {
@@ -3823,6 +3833,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.',
@@ -4124,6 +4141,13 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'always_show_tax',
+    'section'     => 'invoicing',
+    'description' => 'Show a line for tax on the invoice even when the tax is zero.  Optionally provide text for the tax name to show.',
+    'type'        => [ qw(checkbox text) ],
+  },
+
+  {
     'key'         => 'address_standardize_method',
     'section'     => 'UI', #???
     'description' => 'Method for standardizing customer addresses.',
@@ -5198,6 +5222,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?
index 43e9b06..6653fb7 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;
@@ -337,6 +338,7 @@ if ( -e $addl_handler_use_file ) {
   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 a868d48..15636af 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 = '';
@@ -1788,6 +1794,8 @@ sub batch_import {
 
       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 );
       }
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 633e59c..28c7fc4 100644 (file)
@@ -1721,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', '', '', '',
@@ -1742,6 +1743,7 @@ sub tables_hashref {
         'change_pkgnum',       'int', 'NULL', '', '', '',
         'change_pkgpart',      'int', 'NULL', '', '', '',
         'change_locationnum',  'int', 'NULL', '', '', '',
+        'change_custnum',      'int', 'NULL', '', '', '',
         'main_pkgnum',         'int', 'NULL', '', '', '',
         'pkglinknum',          'int', 'NULL', '', '', '',
         'manual_flag',        'char', 'NULL',  1, '', '', 
@@ -2011,6 +2013,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',   '',      '', '', '',
@@ -2715,9 +2730,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'      => [],
@@ -2894,22 +2910,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' ] ],
@@ -3246,7 +3268,8 @@ sub tables_hashref {
         'gateway_username', 'varchar',  'NULL', $char_d, '', '', 
         'gateway_password', 'varchar',  'NULL', $char_d, '', '', 
         'gateway_action',   'varchar',  'NULL', $char_d, '', '', 
-        'gateway_callback_url', 'varchar',  'NULL', $char_d, '', '', 
+        'gateway_callback_url', 'varchar',  'NULL', 255, '', '', 
+        'gateway_cancel_url',   'varchar',  'NULL', 255, '', '',
         'disabled',   'char',  'NULL',   1, '', '', 
       ],
       'primary_key' => 'gatewaynum',
index 324f052..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/;
index e3958a4..dd1796c 100644 (file)
@@ -597,12 +597,54 @@ sub print_generic {
   # info from customer's last invoice before this one, for some 
   # summary formats
   $invoice_data{'last_bill'} = {};
-  my $last_bill = $pr_cust_bill[-1];
+  # returns the last unpaid bill, not the last bill
+  #my $last_bill = $pr_cust_bill[-1];
+  # THIS returns the customer's last bill before  this one
+  my $last_bill = qsearchs({
+      'table'   => 'cust_bill',
+      'hashref' => { 'custnum' => $self->custnum,
+                     'invnum'  => { op => '<', value => $self->invnum },
+                   },
+      'order_by'  => ' ORDER BY invnum DESC LIMIT 1'
+  });
   if ( $last_bill ) {
     $invoice_data{'last_bill'} = {
       '_date'     => $last_bill->_date, #unformatted
       # all we need for now
     };
+    my (@payments, @credits);
+    # for formats that itemize previous payments
+    foreach my $cust_pay ( qsearch('cust_pay', {
+                            'custnum' => $self->custnum,
+                            '_date'   => { op => '>=',
+                                           value => $last_bill->_date }
+                           } ) )
+    {
+      next if $cust_pay->_date > $self->_date;
+      push @payments, {
+          '_date'       => $cust_pay->_date,
+          'date'        => time2str($date_format, $cust_pay->_date),
+          'payinfo'     => $cust_pay->payby_payinfo_pretty,
+          'amount'      => sprintf('%.2f', $cust_pay->paid),
+      };
+      # not concerned about applications
+    }
+    foreach my $cust_credit ( qsearch('cust_credit', {
+                            'custnum' => $self->custnum,
+                            '_date'   => { op => '>=',
+                                           value => $last_bill->_date }
+                           } ) )
+    {
+      next if $cust_credit->_date > $self->_date;
+      push @credits, {
+          '_date'       => $cust_credit->_date,
+          'date'        => time2str($date_format, $cust_credit->_date),
+          'creditreason'=> $cust_credit->cust_credit->reason,
+          'amount'      => sprintf('%.2f', $cust_credit->amount),
+      };
+    }
+    $invoice_data{'previous_payments'} = \@payments;
+    $invoice_data{'previous_credits'}  = \@credits;
   }
 
   my $summarypage = '';
@@ -687,6 +729,11 @@ sub print_generic {
   my $other_money_char = $other_money_chars{$format};
   $invoice_data{'dollar'} = $other_money_char;
 
+  my %minus_signs = ( 'latex'    => '$-$',
+                      'html'     => '&minus;',
+                      'template' => '- ' );
+  my $minus = $minus_signs{$format};
+
   my @detail_items = ();
   my @total_items = ();
   my @buf = ();
@@ -971,7 +1018,8 @@ sub print_generic {
   warn "$me adding taxes\n"
     if $DEBUG > 1;
 
-  foreach my $tax ( $self->_items_tax ) {
+  my @items_tax = $self->_items_tax;
+  foreach my $tax ( @items_tax ) {
 
     $taxtotal += $tax->{'amount'};
 
@@ -1006,7 +1054,7 @@ sub print_generic {
 
   }
   
-  if ( $taxtotal ) {
+  if ( @items_tax ) {
     my $total = {};
     $total->{'total_item'} = $self->mt('Sub-total');
     $total->{'total_amount'} =
@@ -1106,7 +1154,7 @@ sub print_generic {
         my $total;
         $total->{'total_item'} = &$escape_function($credit->{'description'});
         $credittotal += $credit->{'amount'};
-        $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
+        $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
         $adjusttotal += $credit->{'amount'};
         if ( $multisection ) {
           my $money = $old_latex ? '' : $money_char;
@@ -1137,7 +1185,7 @@ sub print_generic {
         my $total = {};
         $total->{'total_item'} = &$escape_function($payment->{'description'});
         $paymenttotal += $payment->{'amount'};
-        $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
+        $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
         $adjusttotal += $payment->{'amount'};
         if ( $multisection ) {
           my $money = $old_latex ? '' : $money_char;
@@ -2129,7 +2177,17 @@ sub _taxsort {
 sub _items_tax {
   my $self = shift;
   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
-  $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+  my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+
+  if ( $self->conf->exists('always_show_tax') ) {
+    my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
+    if (0 == grep { $_->{description} eq $itemdesc } @items) {
+      push @items,
+        { 'description' => $itemdesc,
+          'amount'      => 0.00 };
+    }
+  }
+  @items;
 }
 
 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
@@ -2181,6 +2239,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 );
@@ -2225,7 +2284,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;
 
index c11e6c9..f63854c 100644 (file)
@@ -472,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;
 }
 
@@ -578,7 +581,7 @@ use vars qw($DEBUG);
 use Carp;
 use Storable qw(nfreeze);
 use MIME::Base64;
-use JSON;
+use JSON::XS;
 use FS::CurrentUser;
 use FS::Record qw(qsearchs);
 use FS::queue;
@@ -723,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 0e8bf45..5bcf922 100644 (file)
@@ -229,7 +229,10 @@ sub _upgrade_data { # class method
                             'Usage: Unrateable CDRs',
                           ],
     'Provision customer service' => [ 'Edit password' ],
-
+    'Financial reports' => [ 'Employees: Commission Report',
+                             'Employees: Audit Report',
+                           ],
+    'Change customer package' => 'Detach customer package',
 ;
 
   foreach my $old_acl ( keys %onetime ) {
index 3794d3f..9b32209 100644 (file)
@@ -216,7 +216,7 @@ an attempt will be made to select a gateway suited for the taxes paid on
 the invoice.
 
 The I<method> and I<payinfo> options can be used to influence the choice
-as well.  Presently only 'CC' and 'ECHECK' methods are meaningful.
+as well.  Presently only 'CC', 'ECHECK', and 'PAYPAL' methods are meaningful.
 
 When the I<method> is 'CC' then the card number in I<payinfo> can direct
 this routine to route to a gateway suited for that type of card.
@@ -246,13 +246,17 @@ sub payment_gateway {
   }
 
   #look for an agent gateway override first
-  my $cardtype;
-  if ( $options{method} && $options{method} eq 'CC' && $options{payinfo} ) {
-    $cardtype = cardtype($options{payinfo});
-  } elsif ( $options{method} && $options{method} eq 'ECHECK' ) {
-    $cardtype = 'ACH';
-  } else {
-    $cardtype = $options{method} || '';
+  my $cardtype = '';
+  if ( $options{method} ) {
+    if ( $options{method} eq 'CC' && $options{payinfo} ) {
+      $cardtype = cardtype($options{payinfo});
+    } elsif ( $options{method} eq 'ECHECK' ) {
+      $cardtype = 'ACH';
+    } elsif ( $options{method} eq 'PAYPAL' ) {
+      $cardtype = 'PayPal';
+    } else {
+      $cardtype = $options{method}
+    }
   }
 
   my $override =
index b1496ac..275e7b3 100644 (file)
@@ -6,6 +6,7 @@ use vars qw( %info %TZ );
 use Time::Local;
 #use Data::Dumper;
 
+#false laziness w/huawei_softx3000.pm
 %TZ = (
   '+0000' => 'XXX-0',
   '+0100' => 'XXX-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;
+
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 9bab493..fc6a7dd 100644 (file)
@@ -110,9 +110,11 @@ Customer info at invoice generation time
 
 =over 4
 
-=item previous_balance
+=item billing_balance - the customer's balance at the time the invoice was 
+generated (not including charges on this invoice)
 
-=item billing_balance
+=item previous_balance - the billing_balance of this customer's previous 
+invoice plus the charges on that invoice
 
 =back
 
@@ -2144,6 +2146,7 @@ sub print_csv {
       $self->custnum,
       $cust_main->first,
       $cust_main->last,
+      $cust_main->company,
       $cust_main->address1,
       $cust_main->address2,
       $cust_main->city,
@@ -3137,11 +3140,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..0c8c0bb 100644 (file)
@@ -1104,16 +1104,12 @@ 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 });
-    if ( !$tax_loc->locationnum ) {
-      $tax_loc->disabled('Y');
-      my $error = $tax_loc->insert;
-      if ( $error ) {
-        warn "couldn't create historical location record for cust#".
-        $h_cust_main->custnum.": $error\n";
-        next INVOICE;
-      }
+    my $tax_loc = FS::cust_location->new(\%hash);
+    my $error = $tax_loc->find_or_insert || $tax_loc->disable_if_unused;
+    if ( $error ) {
+      warn "couldn't create historical location record for cust#".
+      $h_cust_main->custnum.": $error\n";
+      next INVOICE;
     }
     my $exempt_cust = 1 if $h_cust_main->tax;
 
index b25163f..4560716 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,95 @@ points to.  You can ask the object for a copy with the I<hash> method.
 
 sub table { 'cust_location'; }
 
+=item find_or_insert
+
+Finds an existing location matching the customer and address values in this
+location, if one exists, and sets the contents of this location equal to that
+one (including its locationnum).
+
+If an existing location is not found, this one I<will> be inserted.  (This is a
+change from the "new_or_existing" method that this replaces.)
+
+The following fields are considered "essential" and I<must> match: custnum,
+address1, address2, city, county, state, zip, country, location_number,
+location_type, location_kind.  Disabled locations will be found only if this
+location is set to disabled.
+
+If 'coord_auto' is null, and latitude and longitude are not null, then 
+latitude and longitude are also essential fields.
+
+All other fields are considered "non-essential".  If a non-essential field is
+empty in this location, it will be ignored in determining whether an existing
+location matches.
+
+If a non-essential field is non-empty in this location, existing locations 
+that contain a different non-empty value for that field will not match.  An 
+existing location in which the field is I<empty> will match, but will be 
+updated in-place with the value of that field.
+
+Returns an error string if inserting or updating a location failed.
+
+It is unfortunately hard to determine if this created a new location or not.
+
+=cut
+
+sub find_or_insert {
+  my $self = shift;
+
+  my @essential = (qw(custnum address1 address2 city county state zip country
+    location_number location_type location_kind disabled));
+
+  if ( !$self->coord_auto and $self->latitude and $self->longitude ) {
+    push @essential, qw(latitude longitude);
+    # but NOT coord_auto; if the latitude and longitude match the geocoded
+    # values then that's good enough
+  }
+
+  # put nonempty, nonessential fields/values into this hash
+  my %nonempty = map { $_ => $self->get($_) }
+                 grep {$self->get($_)} $self->fields;
+  delete @nonempty{@essential};
+  delete $nonempty{'locationnum'};
+
+  my %hash = map { $_ => $self->get($_) } @essential;
+  my @matches = qsearch('cust_location', \%hash);
+
+  # consider candidate locations
+  MATCH: foreach my $old (@matches) {
+    my $reject = 0;
+    foreach my $field (keys %nonempty) {
+      my $old_value = $old->get($field);
+      if ( length($old_value) > 0 ) {
+        if ( $field eq 'latitude' or $field eq 'longitude' ) {
+          # special case, because these are decimals
+          if ( abs($old_value - $nonempty{$field}) > 0.000001 ) {
+            $reject = 1;
+          }
+        } elsif ( $old_value ne $nonempty{$field} ) {
+          $reject = 1;
+        }
+      } else {
+        # it's empty in $old, has a value in $self
+        $old->set($field, $nonempty{$field});
+      }
+      next MATCH if $reject;
+    } # foreach $field
+
+    if ( $old->modified ) {
+      my $error = $old->replace;
+      return $error if $error;
+    }
+    # set $self equal to $old
+    foreach ($self->fields) {
+      $self->set($_, $old->get($_));
+    }
+    return "";
+  }
+
+  # didn't find a match
+  return $self->insert;
+}
+
 =item insert
 
 Adds this record to the database.  If there is an error, returns the error,
index 3c0702f..1d6e845 100644 (file)
@@ -390,7 +390,7 @@ sub insert {
 
     $payby = 'PREP' if $amount;
 
-  } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) {
+  } elsif ( $self->payby =~ /^(CASH|WEST|MCRD|PPAL)$/ ) {
 
     $payby = $1;
     $self->payby('BILL');
@@ -1509,43 +1509,17 @@ sub replace {
     my $old_loc = $old->$l;
     my $new_loc = $self->$l;
 
-    if ( !$new_loc->locationnum ) {
-      # changing location
-      # If the new location is all empty fields, or if it's identical to 
-      # the old location in all fields, don't replace.
-      my @nonempty = grep { $new_loc->$_ } $self->location_fields;
-      next if !@nonempty;
-      my @unlike = grep { $new_loc->$_ ne $old_loc->$_ } $self->location_fields;
-
-      if ( @unlike or $old_loc->disabled ) {
-        warn "  changed $l fields: ".join(',',@unlike)."\n"
-          if $DEBUG;
-        $new_loc->set(custnum => $self->custnum);
-
-        # insert it--the old location will be disabled later
-        my $error = $new_loc->insert;
-        if ( $error ) {
-          $dbh->rollback if $oldAutoCommit;
-          return $error;
-        }
-
-      } else {
-      # no fields have changed and $old_loc isn't disabled, so don't change it
-        next;
-      }
-
-    }
-    elsif ( $new_loc->custnum ne $self->custnum or $new_loc->prospectnum ) {
+    # find the existing location if there is one
+    $new_loc->set('custnum' => $self->custnum);
+    my $error = $new_loc->find_or_insert;
+    if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
-      return "$l belongs to customer ".$new_loc->custnum;
+      return $error;
     }
-    # else the new location belongs to this customer so we're good
-
-    # set the foo_locationnum now that we have one.
     $self->set($l.'num', $new_loc->locationnum);
-
   } #for $l
 
+  # replace the customer record
   my $error = $self->SUPER::replace($old);
 
   if ( $error ) {
@@ -2021,7 +1995,8 @@ sub check {
 
   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
     return "Expiration date required"
-      unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
+      # shouldn't payinfo_check do this?
+      unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD|PPAL)$/;
     $self->paydate('');
   } else {
     my( $m, $y );
index 939a625..814802b 100644 (file)
@@ -437,6 +437,24 @@ sub bill {
     my @part_pkg = $cust_pkg->part_pkg->self_and_bill_linked;
     $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
  
+    # if this package was changed from another package,
+    # and it hasn't been billed since then,
+    # and package balances are enabled,
+    if ( $cust_pkg->change_pkgnum
+        and $cust_pkg->change_date >= ($cust_pkg->last_bill || 0)
+        and $cust_pkg->change_date <  $invoice_time
+      and $conf->exists('pkg-balances') )
+    {
+      # _transfer_balance will also create the appropriate credit
+      my @transfer_items = $self->_transfer_balance($cust_pkg);
+      # $part_pkg[0] is the "real" part_pkg
+      my $pass = ($cust_pkg->no_auto || $part_pkg[0]->no_auto) ? 
+                  'no_auto' : '';
+      push @{ $cust_bill_pkg{$pass} }, @transfer_items;
+      # treating this as recur, just because most charges are recur...
+      ${$total_recur{$pass}} += $_->recur foreach @transfer_items;
+    }
+
     foreach my $part_pkg ( @part_pkg ) {
 
       $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
@@ -1220,24 +1238,107 @@ sub _make_lines {
 
 }
 
-# This is _handle_taxes.  It's called once for each cust_bill_pkg generated
-# from _make_lines, along with the part_pkg, cust_pkg, invoice time, the 
-# non-overridden pkgpart, a flag indicating whether the package is being
-# canceled, and a partridge in a pear tree.
-#
-# The most important argument is 'taxlisthash'.  This is shared across the 
-# entire invoice.  It looks like this:
-# {
-#   'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
-#   'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
-# }
-#
-# 'cust_main_county' can also be 'tax_rate'.  The first object in the array
-# is always the cust_main_county or tax_rate identified by the key.
-#
-# That "..." is a list of FS::cust_bill_pkg objects that will be fed to 
-# the 'taxline' method to calculate the amount of the tax.  This doesn't
-# happen until calculate_taxes, though.
+=item _transfer_balance TO_PKG [ FROM_PKGNUM ]
+
+Takes one argument, a cust_pkg object that is being billed.  This will 
+be called only if the package was created by a package change, and has
+not been billed since the package change, and package balance tracking
+is enabled.  The second argument can be an alternate package number to 
+transfer the balance from; this should not be used externally.
+
+Transfers the balance from the previous package (now canceled) to
+this package, by crediting one package and creating an invoice item for 
+the other.  Inserts the credit and returns the invoice item (so that it 
+can be added to an invoice that's being built).
+
+If the previous package was never billed, and was also created by a package
+change, then this will also transfer the balance from I<its> previous 
+package, and so on, until reaching a package that either has been billed
+or was not created by a package change.
+
+=cut
+
+my $balance_transfer_reason;
+
+sub _transfer_balance {
+  my $self = shift;
+  my $cust_pkg = shift;
+  my $from_pkgnum = shift || $cust_pkg->change_pkgnum;
+  my $from_pkg = FS::cust_pkg->by_key($from_pkgnum);
+
+  my @transfers;
+
+  # if $from_pkg is not the first package in the chain, and it was never 
+  # billed, walk back
+  if ( $from_pkg->change_pkgnum and scalar($from_pkg->cust_bill_pkg) == 0 ) {
+    @transfers = $self->_transfer_balance($cust_pkg, $from_pkg->change_pkgnum);
+  }
+
+  my $prev_balance = $self->balance_pkgnum($from_pkgnum);
+  if ( $prev_balance != 0 ) {
+    $balance_transfer_reason ||= FS::reason->new_or_existing(
+      'reason' => 'Package balance transfer',
+      'type'   => 'Internal adjustment',
+      'class'  => 'R'
+    );
+
+    my $credit = FS::cust_credit->new({
+        'custnum'   => $self->custnum,
+        'amount'    => abs($prev_balance),
+        'reasonnum' => $balance_transfer_reason->reasonnum,
+        '_date'     => $cust_pkg->change_date,
+    });
+
+    my $cust_bill_pkg = FS::cust_bill_pkg->new({
+        'setup'     => 0,
+        'recur'     => abs($prev_balance),
+        #'sdate'     => $from_pkg->last_bill, # not sure about this
+        #'edate'     => $cust_pkg->change_date,
+        'itemdesc'  => $self->mt('Previous Balance, [_1]',
+                                 $from_pkg->part_pkg->pkg),
+    });
+
+    if ( $prev_balance > 0 ) {
+      # credit the old package, charge the new one
+      $credit->set('pkgnum', $from_pkgnum);
+      $cust_bill_pkg->set('pkgnum', $cust_pkg->pkgnum);
+    } else {
+      # the reverse
+      $credit->set('pkgnum', $cust_pkg->pkgnum);
+      $cust_bill_pkg->set('pkgnum', $from_pkgnum);
+    }
+    my $error = $credit->insert;
+    die "error transferring package balance from #".$from_pkgnum.
+        " to #".$cust_pkg->pkgnum.": $error\n" if $error;
+
+    push @transfers, $cust_bill_pkg;
+  } # $prev_balance != 0
+
+  return @transfers;
+}
+
+=item _handle_taxes PART_PKG TAXLISTHASH CUST_BILL_PKG CUST_PKG TIME PKGPART [ OPTIONS ]
+
+This is _handle_taxes.  It's called once for each cust_bill_pkg generated
+from _make_lines, along with the part_pkg, cust_pkg, invoice time, the 
+non-overridden pkgpart, a flag indicating whether the package is being
+canceled, and a partridge in a pear tree.
+
+The most important argument is 'taxlisthash'.  This is shared across the 
+entire invoice.  It looks like this:
+{
+  'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
+  'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
+}
+
+'cust_main_county' can also be 'tax_rate'.  The first object in the array
+is always the cust_main_county or tax_rate identified by the key.
+
+That "..." is a list of FS::cust_bill_pkg objects that will be fed to 
+the 'taxline' method to calculate the amount of the tax.  This doesn't
+happen until calculate_taxes, though.
+
+=cut
 
 sub _handle_taxes {
   my $self = shift;
index 804969b..1caa3e5 100644 (file)
@@ -111,7 +111,7 @@ L<http://420.am/business-onlinepayment> for supported gateways.
 
 Required arguments in the hashref are I<method>, and I<amount>
 
-Available methods are: I<CC>, I<ECHECK> and I<LEC>
+Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
 
 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
 
@@ -317,6 +317,7 @@ my %bop_method2payby = (
   'CC'     => 'CARD',
   'ECHECK' => 'CHEK',
   'LEC'    => 'LECB',
+  'PAYPAL' => 'PPAL',
 );
 
 sub realtime_bop {
@@ -612,6 +613,7 @@ sub realtime_bop {
     %$bop_content,
     'reference'      => $cust_pay_pending->paypendingnum, #for now
     'callback_url'   => $payment_gateway->gateway_callback_url,
+    'cancel_url'     => $payment_gateway->gateway_cancel_url,
     'email'          => $email,
     %content, #after
   );
index 588f8a1..8484df5 100644 (file)
@@ -4,9 +4,11 @@ use strict;
 use vars qw( $DEBUG $me );
 use List::Util qw( min );
 use FS::UID qw( dbh );
-use FS::Record qw( qsearch );
+use FS::Record qw( qsearch qsearchs );
 use FS::cust_pkg;
 use FS::cust_svc;
+use FS::contact;       # for attach_pkgs
+use FS::cust_location; #
 
 $DEBUG = 0;
 $me = '[FS::cust_main::Packages]';
@@ -87,7 +89,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';
@@ -100,17 +102,45 @@ 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 ( $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->contactnum($opt->{'contact'}->contactnum);
+
+  #} else {
+  #
+  #  $cust_pkg->contactnum();
+
+  }
+
+  if ( $opt->{'locationnum'} and $opt->{'locationnum'} != -1 ) {
+
+    $cust_pkg->locationnum($opt->{'locationnum'});
+
+  } elsif ( $opt->{'cust_location'} ) {
+
+    my $error = $opt->{'cust_location'}->find_or_insert;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return "inserting cust_location (transaction rolled back): $error";
     }
     $cust_pkg->locationnum($opt->{'cust_location'}->locationnum);
-  }
-  else {
+
+  } else {
+
     $cust_pkg->locationnum($self->ship_locationnum);
+
   }
 
   $cust_pkg->custnum( $self->custnum );
@@ -164,6 +194,7 @@ sub order_pkg {
         '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 ) {
@@ -259,6 +290,108 @@ sub order_pkgs {
   ''; #no error
 }
 
+=item attach_pkgs 
+
+Merges this customer's package's into the target customer and then cancels them.
+
+=cut
+
+sub attach_pkgs {
+  my( $self, $new_custnum ) = @_;
+
+  #mostly false laziness w/ merge
+
+  return "Can't attach packages to self" if $self->custnum == $new_custnum;
+
+  my $new_cust_main = qsearchs( 'cust_main', { 'custnum' => $new_custnum } )
+    or return "Invalid new customer number: $new_custnum";
+
+  return 'Access denied: "Merge customer across agents" access right required to merge into a customer of a different agent'
+    if $self->agentnum != $new_cust_main->agentnum 
+    && ! $FS::CurrentUser::CurrentUser->access_right('Merge customer across agents');
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  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;
+
+  if ( qsearch('agent', { 'agent_custnum' => $self->custnum } ) ) {
+     $dbh->rollback if $oldAutoCommit;
+     return "Can't merge a master agent customer";
+  }
+
+  #use FS::access_user
+  if ( qsearch('access_user', { 'user_custnum' => $self->custnum } ) ) {
+     $dbh->rollback if $oldAutoCommit;
+     return "Can't merge a master employee customer";
+  }
+
+  if ( qsearch('cust_pay_pending', { 'custnum' => $self->custnum,
+                                     'status'  => { op=>'!=', value=>'done' },
+                                   }
+              )
+  ) {
+     $dbh->rollback if $oldAutoCommit;
+     return "Can't merge a customer with pending payments";
+  }
+
+  #end of false laziness
+
+  #pull in contact
+
+  my %contact_hash = ( 'first'    => $self->first,
+                       'last'     => $self->get('last'),
+                       'custnum'  => $new_custnum,
+                       'disabled' => '',
+                     );
+
+  my $contact = qsearchs(  'contact', \%contact_hash)
+                 || new FS::contact   \%contact_hash;
+  unless ( $contact->contactnum ) {
+    my $error = $contact->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
+
+    my $cust_location = $cust_pkg->cust_location || $self->ship_location;
+    my %loc_hash = $cust_location->hash;
+    $loc_hash{'locationnum'} = '';
+    $loc_hash{'custnum'}     = $new_custnum;
+    $loc_hash{'disabled'}    = '';
+    my $new_cust_location = qsearchs(  'cust_location', \%loc_hash)
+                             || new FS::cust_location   \%loc_hash;
+
+    my $pkg_or_error = $cust_pkg->change( {
+      'keep_dates'    => 1,
+      'cust_main'     => $new_cust_main,
+      'contactnum'    => $contact->contactnum,
+      'cust_location' => $new_cust_location,
+    } );
+
+    my $error = ref($pkg_or_error) ? '' : $pkg_or_error;
+
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  ''; #no error
+
+}
+
 =item all_pkgs [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
 
 Returns all packages (see L<FS::cust_pkg>) for this customer.
index d8f620f..e0c7080 100644 (file)
@@ -624,14 +624,14 @@ sub search {
   # parse without census tract checkbox
   ##
 
-  push @where, "(censustract = '' or censustract is null)"
+  push @where, "(ship_location.censustract = '' or ship_location.censustract is null)"
     if $params->{'no_censustract'};
 
   ##
   # parse with hardcoded tax location checkbox
   ##
 
-  push @where, "geocode is not null"
+  push @where, "ship_location.geocode is not null"
     if $params->{'with_geocode'};
 
   ##
@@ -841,7 +841,7 @@ sub search {
       'ON (cust_main.'.$pre.'locationnum = '.$pre.'location.locationnum) ';
   }
 
-  my $count_query = "SELECT COUNT(*) FROM cust_main $extra_sql";
+  my $count_query = "SELECT COUNT(*) FROM cust_main $addl_from $extra_sql";
 
   my @select = (
                  'cust_main.custnum',
@@ -857,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') {
 
@@ -926,6 +927,8 @@ sub search {
     'extra_headers' => \@extra_headers,
     'extra_fields'  => \@extra_fields,
   };
+  warn Data::Dumper::Dumper($sql_query);
+  $sql_query;
 
 }
 
@@ -953,6 +956,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 ) {
 
@@ -960,7 +968,7 @@ sub fuzzy_search {
     next unless scalar(@$all);
 
     my %match = ();
-    $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) );
+    $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, \@fuzzy_mod, @$all ) );
     next if !keys(%match);
 
     my $in_matches = 'IN (' .
index 9a4990a..a61d67e 100644 (file)
@@ -147,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
 
@@ -175,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;
index 4491f78..da91439 100644 (file)
@@ -1062,6 +1062,8 @@ sub _upgrade_data {  #class method
             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
@@ -1079,7 +1081,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 19337c4..4dced54 100644 (file)
@@ -1,7 +1,8 @@
 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);
@@ -17,6 +18,7 @@ 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;
@@ -225,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.
@@ -267,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
@@ -274,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;
@@ -613,7 +622,7 @@ sub check {
     $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')
@@ -654,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 }
@@ -848,6 +862,7 @@ sub cancel {
 
   my %hash = $self->hash;
   $date ? ($hash{'expire'} = $date) : ($hash{'cancel'} = $cancel_time);
+  $hash{'change_custnum'} = $options{'change_custnum'};
   my $new = new FS::cust_pkg ( \%hash );
   $error = $new->replace( $self, options => { $self->options } );
   if ( $error ) {
@@ -981,6 +996,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;
@@ -1022,15 +1038,20 @@ sub uncancel {
         $dbh->rollback if $oldAutoCommit;
         return $svc_error;
       } else {
+        # if we've failed to insert the svc_x object, svc_Common->insert 
+        # will have removed the cust_svc already.  if not, then both records
+        # were inserted but we failed for some other reason (export, most 
+        # likely).  in that case, report the error and delete the records.
         push @svc_errors, $svc_error;
-        # is this necessary? svc_Common::insert already deletes the 
-        # cust_svc if inserting svc_x fails.
         my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $svc_x->svcnum });
         if ( $cust_svc ) {
-          my $cs_error = $cust_svc->delete;
-          if ( $cs_error ) {
+          # except if export_insert failed, export_delete probably won't be
+          # much better
+          local $FS::svc_Common::noexport_hack = 1;
+          my $cleanup_error = $svc_x->delete; # also deletes cust_svc
+          if ( $cleanup_error ) { # and if THAT fails, then run away
             $dbh->rollback if $oldAutoCommit;
-            return $cs_error;
+            return $cleanup_error;
           }
         }
       } # svc_fatal
@@ -1683,6 +1704,11 @@ New locationnum, to change the location for this package.
 New FS::cust_location object, to create a new location and assign it
 to this package.
 
+=item cust_main
+
+New FS::cust_main object, to create a new customer and assign the new package
+to it.
+
 =item pkgpart
 
 New pkgpart (see L<FS::part_pkg>).
@@ -1747,9 +1773,8 @@ sub change {
   $hash{"change_$_"}  = $self->$_()
     foreach qw( pkgnum pkgpart locationnum );
 
-  if ( $opt->{'cust_location'} &&
-       ( ! $opt->{'locationnum'} || $opt->{'locationnum'} == -1 ) ) {
-    $error = $opt->{'cust_location'}->insert;
+  if ( $opt->{'cust_location'} ) {
+    $error = $opt->{'cust_location'}->find_or_insert;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return "inserting cust_location (transaction rolled back): $error";
@@ -1757,6 +1782,12 @@ sub change {
     $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;
   my $keep_dates = $opt->{'keep_dates'};
   # Special case.  If the pkgpart is changing, and the customer is
@@ -1781,15 +1812,37 @@ 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;
+
+  my $custnum = $self->custnum;
+  if ( $opt->{cust_main} ) {
+    my $cust_main = $opt->{cust_main};
+    unless ( $cust_main->custnum ) { 
+      my $error = $cust_main->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "inserting cust_main (transaction rolled back): $error";
+      }
+    }
+    $custnum = $cust_main->custnum;
+  }
+
+  $hash{'contactnum'} = $opt->{'contactnum'} if $opt->{'contactnum'};
+
   # Create the new package.
   my $cust_pkg = new FS::cust_pkg {
-    custnum      => $self->custnum,
-    pkgpart      => ( $opt->{'pkgpart'}     || $self->pkgpart      ),
-    refnum       => ( $opt->{'refnum'}      || $self->refnum       ),
-    locationnum  => ( $opt->{'locationnum'}                        ),
+    custnum        => $custnum,
+    pkgpart        => ( $opt->{'pkgpart'}     || $self->pkgpart      ),
+    refnum         => ( $opt->{'refnum'}      || $self->refnum       ),
+    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;
@@ -1847,6 +1900,23 @@ sub change {
     }
   }
 
+  # 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;
@@ -1864,7 +1934,7 @@ sub change {
     my $new = FS::cust_pkg->new({
         pkgpart       => $link->dst_pkgpart,
         pkglinknum    => $link->pkglinknum,
-        custnum       => $self->custnum,
+        custnum       => $custnum,
         main_pkgnum   => $cust_pkg->pkgnum,
         locationnum   => $cust_pkg->locationnum,
         start_date    => $cust_pkg->start_date,
@@ -1874,14 +1944,14 @@ sub change {
         contract_end  => $cust_pkg->contract_end,
         refnum        => $cust_pkg->refnum,
         discountnum   => $cust_pkg->discountnum,
-        waive_setup   => $cust_pkg->waive_setup
+        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;
+    $error = $new->insert( allow_pkgpart => $same_pkgpart );
     # transfer services
     if ( $old ) {
       $error ||= $old->transfer($new);
@@ -1905,9 +1975,10 @@ sub change {
   #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,
-    nobill        => $keep_dates
+    quiet          => 1, 
+    unused_credit  => $unused_credit,
+    nobill         => $keep_dates,
+    change_custnum => ( $self->custnum != $custnum ? $custnum : '' ),
   );
   if ($error) {
     $dbh->rollback if $oldAutoCommit;
@@ -2079,6 +2150,18 @@ sub old_cust_pkg {
   qsearchs('cust_pkg', { 'pkgnum' => $self->change_pkgnum } );
 }
 
+=item change_cust_main
+
+Returns the customter this package was detached to, if any.
+
+=cut
+
+sub change_cust_main {
+  my $self = shift;
+  return '' unless $self->change_custnum;
+  qsearchs('cust_main', { 'custnum' => $self->change_custnum } );
+}
+
 =item calc_setup
 
 Calls the I<calc_setup> of the FS::part_pkg object associated with this billing
@@ -2639,7 +2722,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
 
@@ -2666,6 +2749,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.
index bbf4eed..6274107 100644 (file)
@@ -863,38 +863,82 @@ sub smart_search_param {
   my @or = 
       map { my $table = $_;
             my $search_sql = "FS::$table"->search_sql($string);
-            " ( svcdb = '$table'
-               AND 0 < ( SELECT COUNT(*) FROM $table
-                           WHERE $table.svcnum = cust_svc.svcnum
-                             AND $search_sql
-                       )
-             ) ";
+
+            "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
+            "FROM $table WHERE $search_sql";
           }
       FS::part_svc->svc_tables;
 
   if ( $string =~ /^(\d+)$/ ) {
-    unshift @or, " ( agent_svcid IS NOT NULL AND agent_svcid = $1 ) ";
+    unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
   }
 
-  my @extra_sql = ' ( '. join(' OR ', @or). ' ) ';
+  my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
+                  " ON (svc_all.svcnum = cust_svc.svcnum) ";
+
+  my @extra_sql;
 
   push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
     'null_right' => 'View/link unlinked services'
   );
   my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
   #for agentnum
-  my $addl_from = ' LEFT JOIN cust_pkg  USING ( pkgnum  )'.
+  $addl_from  .=  ' LEFT JOIN cust_pkg  USING ( pkgnum  )'.
                   FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
                   ' LEFT JOIN part_svc  USING ( svcpart )';
 
   (
     'table'     => 'cust_svc',
+    'select'    => 'svc_all.svcnum AS svcnum, '.
+                   'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb',
     'addl_from' => $addl_from,
     'hashref'   => {},
     'extra_sql' => $extra_sql,
   );
 }
 
+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 1a9bf5a..4293b2c 100644 (file)
@@ -199,13 +199,15 @@ sub batch_import {
       if (exists($hash->{actionflag}) && $hash->{actionflag} eq 'D') {
         delete($hash->{actionflag});
 
-        my $cust_tax_location = qsearchs('cust_tax_location', $hash);
+        my @cust_tax_location = qsearch('cust_tax_location', $hash);
         return "Can't find cust_tax_location to delete: ".
                join(" ", map { "$_ => ". $hash->{$_} } @fields)
-          unless $cust_tax_location;
+          unless scalar(@cust_tax_location) || $param->{'delete_only'} ;
 
-        my $error = $cust_tax_location->delete;
-        return $error if $error;
+        foreach my $cust_tax_location (@cust_tax_location) {
+          my $error = $cust_tax_location->delete;
+          return $error if $error;
+        }
 
         delete($hash->{$_}) foreach (keys %$hash);
       }
@@ -234,13 +236,15 @@ sub batch_import {
       if (exists($hash->{actionflag}) && $hash->{actionflag} eq 'D') {
         delete($hash->{actionflag});
 
-        my $cust_tax_location = qsearchs('cust_tax_location', $hash);
+        my @cust_tax_location = qsearch('cust_tax_location', $hash);
         return "Can't find cust_tax_location to delete: ".
                join(" ", map { "$_ => ". $hash->{$_} } @fields)
-          unless $cust_tax_location;
+          unless scalar(@cust_tax_location) || $param->{'delete_only'} ;
 
-        my $error = $cust_tax_location->delete;
-        return $error if $error;
+        foreach my $cust_tax_location (@cust_tax_location) {
+          my $error = $cust_tax_location->delete;
+          return $error if $error;
+        }
 
         delete($hash->{$_}) foreach (keys %$hash);
       }
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 68288d0..cd9e200 100644 (file)
@@ -17,14 +17,25 @@ sub option_fields {
                     type=>'checkbox', value=>'Y' },
     'nextbill' => { label=>'Hold late fee until next invoice',
                     type=>'checkbox', value=>'Y' },
+    'limit_to_credit'=>
+                  { label=>"Charge no more than the customer's credit balance",
+                    type=>'checkbox', value=>'Y' },
   );
 }
 
 sub default_weight { 10; }
 
 sub _calc_fee {
-  #my( $self, $cust_object ) = @_;
-  my $self = shift;
+  my( $self, $cust_object ) = @_;
+  if ( $self->option('limit_to_credit') ) {
+    my $balance = $cust_object->cust_main->balance;
+    if ( $balance >= 0 ) {
+      return 0;
+    } elsif ( (-1 * $balance) < $self->option('charge') ) {
+      return -1 * $balance;
+    }
+  }
+
   $self->option('charge');
 }
 
@@ -44,6 +55,9 @@ sub do_action {
     'setuptax' => $self->option('setuptax'),
   );
 
+  # amazingly, FS::cust_main::charge will allow a charge of zero
+  return '' if $charge{'amount'} == 0;
+
   #unless its more than N months away?
   $charge{'start_date'} = $cust_main->next_bill_date
     if $self->option('nextbill');
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;
diff --git a/FS/FS/part_event/Condition/inactive_age.pm b/FS/FS/part_event/Condition/inactive_age.pm
new file mode 100644 (file)
index 0000000..8918a1a
--- /dev/null
@@ -0,0 +1,46 @@
+package FS::part_event::Condition::inactive_age;
+
+use strict;
+use base qw( FS::part_event::Condition );
+use FS::Record qw( qsearch );
+
+sub description { 'Days without billing activity' }
+
+sub option_fields {
+  (
+    'age'  =>  { 'label'   => 'No activity within',
+                 'type'    => 'freq',
+               },
+    # flags to select kinds of activity, 
+    # like if you just want "no payments since"?
+    # not relevant yet
+  );
+}
+
+sub condition {
+  my( $self, $obj, %opt ) = @_;
+  my $custnum = $obj->custnum;
+  my $age = $self->option_age_from('age', $opt{'time'} );
+
+  foreach my $t (qw(cust_bill cust_pay cust_credit cust_refund)) {
+    my $class = "FS::$t";
+    return 0 if $class->count("custnum = $custnum AND _date >= $age");
+  }
+  1;
+}
+
+sub condition_sql {
+  my( $class, $table, %opt ) = @_;
+  my $age   = $class->condition_sql_option_age_from('age', $opt{'time'});
+  my @sql;
+  for my $t (qw(cust_bill cust_pay cust_credit cust_refund)) {
+    push @sql,
+      "NOT EXISTS( SELECT 1 FROM $t ".
+      "WHERE $t.custnum = cust_main.custnum AND $t._date >= $age".
+      ")";
+  }
+  join(' AND ', @sql);
+}
+
+1;
+
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
index 15ce9c0..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
@@ -703,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 80139e7..5c4a8d0 100644 (file)
@@ -129,7 +129,7 @@ sub export_setstatus_listdel {
 }
 
 sub export_setstatus_listX {
-  my( $self, $svc_x, $action, $list, $address ) = @_;
+  my( $self, $svc_x, $action, $list, $address_item ) = @_;
 
   my $option;
   if ( $list =~ /^[WA]/i ) { #Whitelist/Allow
@@ -139,8 +139,16 @@ sub export_setstatus_listX {
   }
   $option .= $action. '_url';
 
-  $address = Email::Valid->address($address)
-    or die "address failed $Email::Valid::Details check.\n";
+  my $address;
+  unless ( $address = Email::Valid->address($address_item) ) {
+
+    if ( $address_item =~ /^(\@[\w\-\.]+\.\w{2,63})$/ ) { # "@domain"
+      $address = $1;
+    } else {
+      die "address failed $Email::Valid::Details check.\n";
+    }
+
+  }
 
   #some false laziness w/export_getstatus above
   my $url;
index 0079818..aa09a1c 100644 (file)
@@ -18,16 +18,16 @@ $DEBUG = 0;
 @ISA = qw(FS::part_export);
 
 tie my %options, 'Tie::IxHash',
-  'opname'    => { label=>'Operator login' },
-  'pwd'       => { label=>'Operator password' },
+  'opname'    => { label=>'Operator login (required)' },
+  'pwd'       => { label=>'Operator password (required)' },
   'tplid'     => { label=>'Template number' },
   'hlrsn'     => { label=>'HLR serial number' },
   'k4sno'     => { label=>'K4 serial number' },
-  'cardtype'  => { label  => 'Card type',
+  'cardtype'  => { label  => 'Card type (required)',
                    type   => 'select', 
                    options=> ['SIM', 'USIM']
                  },
-  'alg'       => { label  => 'Authentication algorithm',
+  'alg'       => { label  => 'Authentication algorithm (required)',
                    type   => 'select',
                    options=> ['COMP128_1',
                               'COMP128_2',
@@ -314,8 +314,8 @@ sub import_sim {
     # push IMSI/KI to the HLR
     my $return = $self->command($socket,
       @command,
-      'IMSI', $imsi,
-      'KIVALUE', $ki,
+      'IMSI', qq{"$imsi"},
+      'KIVALUE', qq{"$ki"},
       @args
     );
     if ( $return->{success} ) {
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;
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 856a693..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,6 +17,7 @@ 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;
@@ -24,7 +26,6 @@ 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;
@@ -715,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,
index 153ed56..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,31 +123,46 @@ 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 ) {
+
+    # '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) ) {
-      # calculate the prorated and add'l period charges
+  }
+
+  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;
index d99def2..d0d3e10 100644 (file)
@@ -66,8 +66,13 @@ use Date::Format;
                               'default' => 0,
                             },
 
+    'monthly_cap'        => { 'name' => 'Monthly (billing frequency) cap on all overage charges'.
+                                        ' (0 means no cap)',
+                              'default' => 0,
+                            },
+
   },
-  'fieldorder' => [qw( recur_included_hours recur_hourly_charge recur_hourly_cap recur_included_input recur_input_charge recur_input_cap recur_included_output recur_output_charge recur_output_cap recur_included_total recur_total_charge recur_total_cap global_cap )],
+  'fieldorder' => [qw( recur_included_hours recur_hourly_charge recur_hourly_cap recur_included_input recur_input_charge recur_input_cap recur_included_output recur_output_charge recur_output_cap recur_included_total recur_total_charge recur_total_cap global_cap monthly_cap )],
   'weight' => 41,
 );
 
@@ -79,7 +84,7 @@ sub price_info {
 }
 
 #hacked-up false laziness w/sqlradacct_hour,
-# but keeping it separate to start  with is safer for existing folks
+# but keeping it separate to start with is safer for existing folks
 sub calc_recur {
   my($self, $cust_pkg, $sdate, $details ) = @_;
 
@@ -179,6 +184,10 @@ sub calc_recur {
     $day_start = $tomorrow;
   }
 
+  $charges = $self->option('monthly_cap')
+    if $self->option('monthly_cap')
+    && $charges > $self->option('monthly_cap');
+
   $self->option('recur_fee') + $charges;
 }
 
index 1c891b1..21c6a8a 100644 (file)
@@ -157,10 +157,16 @@ tie my %detail_formats, '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: ',
+    '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_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: ',
@@ -309,6 +315,7 @@ tie my %detail_formats, '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 
@@ -420,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 )
@@ -487,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 ) = @_;
 
@@ -520,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'));
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
   '';
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;
+
index c83f700..a732720 100644 (file)
@@ -5,8 +5,7 @@ use vars qw( @ISA );
 use Date::Parse;
 use DateTime;
 use DateTime::Format::Strptime;
-use FS::UID qw(dbh);
-use FS::Record qw( qsearch qsearchs );
+use FS::Record qw( qsearch qsearchs dbh );
 use FS::part_pkg_taxproduct;
 use FS::Misc qw(csv_from_fixed);
 
@@ -310,8 +309,8 @@ sub batch_import {
           }
         }
 
-        my $part_pkg_taxrate = qsearchs('part_pkg_taxrate', $hash);
-        unless ( $part_pkg_taxrate ) {
+        my @part_pkg_taxrate = qsearch('part_pkg_taxrate', $hash);
+        unless ( scalar(@part_pkg_taxrate) || $param->{'delete_only'} ) {
           if ( $hash->{taxproductnum} ) {
             my $taxproduct =
               qsearchs( 'part_pkg_taxproduct',
@@ -324,8 +323,10 @@ sub batch_import {
                  join(" ", map { "$_ => *". $hash->{$_}. '*' } keys(%$hash) );
         }
 
-        my $error = $part_pkg_taxrate->delete;
-        return $error if $error;
+        foreach my $part_pkg_taxrate (@part_pkg_taxrate) {
+          my $error = $part_pkg_taxrate->delete;
+          return $error if $error;
+        }
 
         delete($hash->{$_}) foreach (keys %$hash);
       }
index a3708d4..b609df3 100644 (file)
@@ -59,7 +59,7 @@ $name = 'BoM';
   footer => sub {
     my ($pay_batch, $batchcount, $batchtotal) = @_;
     sprintf( "YD%08u%014.0f%55s\n", $batchcount, $batchtotal*100, ""). #80
-    sprintf( "Z%014u%05u%014u%05u%40s",  #80 now
+    sprintf( "Z%014.0f%05u%014u%05u%40s", #80 now
       $batchtotal*100, $batchcount, "0", "0", "");
   },
 );
index d0758f4..c069082 100644 (file)
@@ -47,7 +47,7 @@ $DEBUG = 0;
     my $origin = $1;
 
     my $company = $conf->config('company_name', $pay_batch->agentnum);
-    $company = substr($company. (' 'x23), 0, 23);
+    $company = substr(uc($company). (' 'x23), 0, 23);
 
     my $now = time;
 
index d1961a5..e223a05 100644 (file)
@@ -208,6 +208,7 @@ sub longname {
   'CARD' => 'CC',
   'CHEK' => 'ECHECK',
   'MCRD' => 'CC',
+  'PPAL' => 'PAYPAL',
 );
 
 sub payby2bop {
index 9879a3a..8263252 100644 (file)
@@ -44,26 +44,18 @@ For Refunds (cust_refund):
 For Payments (cust_pay):
 'CARD' (credit cards), 'CHEK' (electronic check/ACH),
 'LECB' (phone bill billing), 'BILL' (billing), 'PREP' (prepaid card),
-'CASH' (cash), 'WEST' (Western Union), or 'MCRD' (Manual credit card)
+'CASH' (cash), 'WEST' (Western Union), 'MCRD' (Manual credit card),
+'PPAL' (PayPal)
 'COMP' (free) is depricated as a payment type in cust_pay
 
 =cut 
 
-# was this supposed to do something?
-#sub payby {
-#  my($self,$payby) = @_;
-#  if ( defined($payby) ) {
-#    $self->setfield('payby', $payby);
-#  } 
-#  return $self->getfield('payby')
-#}
-
 =item payinfo
 
 Payment information (payinfo) can be one of the following types:
 
-Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
+Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) 
+prepayment identifier (see L<FS::prepay_credit>), PayPal transaction ID
 
 =cut
 
@@ -267,6 +259,8 @@ sub payby_payinfo_pretty {
     'Western Union'; #. $self->payinfo;
   } elsif ( $self->payby eq 'MCRD' ) {
     'Manual credit card'; #. $self->payinfo;
+  } elsif ( $self->payby eq 'PPAL' ) {
+    'PayPal transaction#' . $self->order_number;
   } else {
     $self->payby. ' '. $self->payinfo;
   }
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 4a7585e..e94a62c 100644 (file)
@@ -41,7 +41,7 @@ currently supported:
 
 =item gateway_namespace - Business::OnlinePayment, Business::OnlineThirdPartyPayment, or Business::BatchPayment
 
-=item gateway_module - Business::OnlinePayment:: module name
+=item gateway_module - Business::OnlinePayment:: (or other) module name
 
 =item gateway_username - payment gateway username
 
@@ -51,6 +51,14 @@ currently supported:
 
 =item disabled - Disabled flag, empty or 'Y'
 
+=item gateway_callback_url - For ThirdPartyPayment only, set to the URL that 
+the user should be redirected to on a successful payment.  This will be sent
+as a transaction parameter (named "callback_url").
+
+=item gateway_cancel_url - For ThirdPartyPayment only, set to the URL that 
+the user should be redirected to if they cancel the transaction.  PayPal
+requires this; other gateways ignore it.
+
 =item auto_resolve_status - For BatchPayment only, set to 'approve' to 
 auto-approve unresolved payments after some number of days, 'reject' to 
 auto-decline them, or null to do nothing.
@@ -128,6 +136,7 @@ sub check {
     || $self->ut_textn('gateway_username')
     || $self->ut_anything('gateway_password')
     || $self->ut_textn('gateway_callback_url')  # a bit too permissive
+    || $self->ut_textn('gateway_cancel_url')
     || $self->ut_enum('disabled', [ '', 'Y' ] )
     || $self->ut_enum('auto_resolve_status', [ '', 'approve', 'reject' ])
     || $self->ut_numbern('auto_resolve_days')
@@ -152,8 +161,8 @@ sub check {
   }
 
   # this little kludge mimics FS::CGI::popurl
-  $self->gateway_callback_url($self->gateway_callback_url. '/')
-    if ( $self->gateway_callback_url && $self->gateway_callback_url !~ /\/$/ );
+  #$self->gateway_callback_url($self->gateway_callback_url. '/')
+  #  if ( $self->gateway_callback_url && $self->gateway_callback_url !~ /\/$/ );
 
   $self->SUPER::check;
 }
index a9a7d74..e6b20db 100644 (file)
@@ -139,6 +139,43 @@ sub reasontype {
 
 =back
 
+=head1 CLASS METHODS
+
+=over 4
+
+=item new_or_existing reason => REASON, type => TYPE, class => CLASS
+
+Fetches the reason matching these parameters if there is one.  If not,
+inserts one.  Will also insert the reason type if necessary.  CLASS must
+be one of 'C' (cancel reasons), 'R' (credit reasons), or 'S' (suspend reasons).
+
+This will die if anything fails.
+
+=cut
+
+sub new_or_existing {
+  my $class = shift;
+  my %opt = @_;
+
+  my $error = '';
+  my %hash = ('class' => $opt{'class'}, 'type' => $opt{'type'});
+  my $reason_type = qsearchs('reason_type', \%hash)
+                    || FS::reason_type->new(\%hash);
+
+  $error = $reason_type->insert unless $reason_type->typenum;
+  die "error inserting reason type: $error\n" if $error;
+
+  %hash = ('reason_type' => $reason_type->typenum, 'reason' => $opt{'reason'});
+  my $reason = qsearchs('reason', \%hash)
+               || FS::reason->new(\%hash);
+
+  $error = $reason->insert unless $reason->reasonnum;
+  die "error inserting reason: $error\n" if $error;
+
+  $reason;
+}
+
+
 =head1 BUGS
 
 Here by termintes.  Don't use on wooden computers.
index e36dbbd..26d6e5b 100644 (file)
@@ -1895,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({
index 01495ca..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,
+                           },
     },
   };
 }
@@ -225,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.
@@ -330,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 96502e4..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+)$/ ) {
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 f28002c..bab8537 100644 (file)
@@ -288,9 +288,8 @@ sub insert {
 
   #false laziness w/cust_pkg.pm... move this to location_Mixin?  that would
   #make it more of a base class than a mixin... :)
-  if ( $options{'cust_location'}
-         && ( ! $self->locationnum || $self->locationnum == -1 ) ) {
-    my $error = $options{'cust_location'}->insert;
+  if ( $options{'cust_location'} ) {
+    my $error = $options{'cust_location'}->find_or_insert;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return "inserting cust_location (transaction rolled back): $error";
@@ -684,7 +683,9 @@ 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".
 
@@ -735,6 +736,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 bfec2c0..d68e7e3 100644 (file)
@@ -5,6 +5,8 @@ use vars qw( @ISA );
 use FS::UID qw(dbh);
 use FS::Record qw( qsearch qsearchs );
 use FS::Misc qw( csv_from_fixed );
+use FS::part_pkg_taxrate;
+use FS::part_pkg_taxoverride;
 
 @ISA = qw(FS::Record);
 
@@ -83,20 +85,53 @@ Delete this record from the database.
 sub delete {
   my $self = shift;
 
-  return "Can't delete a tax class which has tax rates!"
-    if qsearch( 'tax_rate', { 'taxclassnum' => $self->taxclassnum } );
-
-  return "Can't delete a tax class which has package tax rates!"
-    if qsearch( 'part_pkg_taxrate', { 'taxclassnum' => $self->taxclassnum } );
-
   return "Can't delete a tax class which has package tax rates!"
     if qsearch( 'part_pkg_taxrate', { 'taxclassnumtaxed' => $self->taxclassnum } );
 
   return "Can't delete a tax class which has package tax overrides!"
     if qsearch( 'part_pkg_taxoverride', { 'taxclassnum' => $self->taxclassnum } );
 
-  $self->SUPER::delete(@_);
-  
+  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;
+
+  foreach my $tax_rate (
+    qsearch( 'tax_rate', { taxclassnum=>$self->taxclassnum } )
+  ) {
+    my $error = $tax_rate->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  foreach my $part_pkg_taxrate (
+    qsearch( 'part_pkg_taxrate', { taxclassnum=>$self->taxclassnum } )
+  ) {
+    my $error = $part_pkg_taxrate->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  my $error = $self->SUPER::delete(@_);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
 }
 
 =item replace OLD_RECORD
@@ -253,14 +288,23 @@ sub batch_import {
             }
           }
 
-          my $tax_class =
-            new FS::tax_class( { 'data_vendor' => 'cch',
-                                 'taxclass'    => $type->[0].':'.$cat->[0],
-                                 'description' => $type->[1].':'.$cat->[1],
-                             } );
-          my $error = $tax_class->insert;
-          return $error if $error;
+          my %hash = ( 'data_vendor' => 'cch',
+                       'taxclass'    => $type->[0].':'.$cat->[0],
+                       'description' => $type->[1].':'.$cat->[1],
+                     );
+          unless ( qsearchs('tax_class', \%hash) ) {
+            my $tax_class = new FS::tax_class \%hash;
+            my $error = $tax_class->insert;
+
+            return "can't insert tax_class for ".
+                   " old TAXTYPE ". $type->[0].':'.$type->[1].
+                   " and new TAXCAT ". $cat->[0].':'. $cat->[1].
+                   " : $error"
+              if $error;
+          }
+
           $imported++;
+          
         }
       }
 
@@ -283,7 +327,7 @@ sub batch_import {
                                  'description' => $type->[1].':'.$cat->[1],
                              } );
           my $error = $tax_class->insert;
-          return $error if $error;
+          return "can't insert tax_class for new TAXTYPE $type and TAXCAT $cat: $error" if $error;
           $imported++;
         }
       }
@@ -363,7 +407,7 @@ sub batch_import {
   my $error = &{$endhook}();
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
-    return "can't insert tax_class for $line: $error";
+    return "can't run end hook: $error";
   }
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
@@ -378,9 +422,6 @@ sub batch_import {
 
 =head1 BUGS
 
-  batch_import does not handle mixed I and D records in the same file for
-  format cch-update
-
 =head1 SEE ALSO
 
 L<FS::Record>, schema.html from the base documentation.
index a5a623d..342c7cb 100644 (file)
@@ -413,7 +413,7 @@ sub taxline {
   }
 
   my $maxtype = $self->maxtype || 0;
-  if ($maxtype != 0 && $maxtype != 9) {
+  if ($maxtype != 0 && $maxtype != 1 && $maxtype != 9) {
     return $self->_fatal_or_null( 'tax with "'.
                                     $self->maxtype_name. '" threshold'
                                 );
@@ -476,12 +476,12 @@ sub taxline {
 
   }
 
-  #
-  # XXX insert exemption handling here
+  # XXX handle excessrate (use_excessrate) / excessfee /
+  #            taxbase/feebase / taxmax/feemax
+  #            and eventually exemptions
   #
   # the tax or fee is applied to taxbase or feebase and then
   # the excessrate or excess fee is applied to taxmax or feemax
-  #
 
   $amount += $taxable_charged * $self->tax;
   $amount += $taxable_units * $self->fee;
@@ -785,7 +785,8 @@ sub batch_import {
 
   }
 
-  for (grep { !exists($delete{$_}) } keys %insert) {
+  my @replace = grep { exists($delete{$_}) } keys %insert;
+  for (@replace) {
     if ( $job ) {  # progress bar
       if ( time - $min_sec > $last ) {
         my $error = $job->update_statustext(
@@ -799,20 +800,35 @@ sub batch_import {
       }
     }
 
-    my $tax_rate = new FS::tax_rate( $insert{$_} );
-    my $error = $tax_rate->insert;
+    my $old = qsearchs( 'tax_rate', $delete{$_} );
 
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      my $hashref = $insert{$_};
-      $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
-      return "can't insert tax_rate for $line: $error";
+    if ( $old ) {
+
+      my $new = new FS::tax_rate({ $old->hash, %{$insert{$_}}, 'manual' => ''  });
+      $new->taxnum($old->taxnum);
+      my $error = $new->replace($old);
+
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        my $hashref = $insert{$_};
+        $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
+        return "can't replace tax_rate for $line: $error";
+      }
+
+      $imported++;
+
+    } else {
+
+      $old = delete $delete{$_};
+      warn "WARNING: can't find tax_rate to replace (inserting instead and continuing) for: ".
+        #join(" ", map { "$_ => ". $old->{$_} } @fields);
+        join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
     }
 
     $imported++;
   }
 
-  for (grep { exists($delete{$_}) } keys %insert) {
+  for (grep { !exists($delete{$_}) } keys %insert) {
     if ( $job ) {  # progress bar
       if ( time - $min_sec > $last ) {
         my $error = $job->update_statustext(
@@ -826,27 +842,17 @@ sub batch_import {
       }
     }
 
-    my $old = qsearchs( 'tax_rate', $delete{$_} );
-    unless ($old) {
-      $dbh->rollback if $oldAutoCommit;
-      $old = $delete{$_};
-      return "can't find tax_rate to replace for: ".
-        #join(" ", map { "$_ => ". $old->{$_} } @fields);
-        join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
-    }
-    my $new = new FS::tax_rate({ $old->hash, %{$insert{$_}}, 'manual' => ''  });
-    $new->taxnum($old->taxnum);
-    my $error = $new->replace($old);
+    my $tax_rate = new FS::tax_rate( $insert{$_} );
+    my $error = $tax_rate->insert;
 
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       my $hashref = $insert{$_};
       $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
-      return "can't replace tax_rate for $line: $error";
+      return "can't insert tax_rate for $line: $error";
     }
 
     $imported++;
-    $imported++;
   }
 
   for (grep { !exists($insert{$_}) } keys %delete) {
@@ -961,7 +967,7 @@ sub _perform_batch_import {
       my $file = lc($name). 'file';
 
       unless ($files{$file}) {
-        $error = "No $name supplied";
+        #$error = "No $name supplied";
         next;
       }
       next if $name eq 'DETAIL' && $format =~ /update/;
@@ -978,7 +984,7 @@ sub _perform_batch_import {
         unlink $filename or warn "Can't delete $filename: $!"
           unless $keep_cch_files;
         push @insert_list, $name, $insertname, $import_sub, $format;
-        if ( $name eq 'GEOCODE' ) { #handle this whole ordering issue better
+        if ( $name eq 'GEOCODE' || $name eq 'CODE' ) { #handle this whole ordering issue better
           unshift @predelete_list, $name, $deletename, $import_sub, $format;
         } else {
           unshift @delete_list, $name, $deletename, $import_sub, $format;
@@ -996,10 +1002,17 @@ sub _perform_batch_import {
       'DETAIL', "$dir/".$files{detailfile}, \&FS::tax_rate::batch_import, $format
       if $format =~ /update/;
 
+    my %addl_param = ();
+    if ( $param->{'delete_only'} ) {
+      $addl_param{'delete_only'} = $param->{'delete_only'};
+      @insert_list = () 
+    }
+
     $error ||= _perform_cch_tax_import( $job,
                                         [ @predelete_list ],
                                         [ @insert_list ],
                                         [ @delete_list ],
+                                        \%addl_param,
     );
     
     
@@ -1024,7 +1037,8 @@ sub _perform_batch_import {
 
 
 sub _perform_cch_tax_import {
-  my ( $job, $predelete_list, $insert_list, $delete_list ) = @_;
+  my ( $job, $predelete_list, $insert_list, $delete_list, $addl_param ) = @_;
+  $addl_param ||= {};
 
   my $error = '';
   foreach my $list ($predelete_list, $insert_list, $delete_list) {
@@ -1033,7 +1047,11 @@ sub _perform_cch_tax_import {
       my $fmt = "$format-update";
       $fmt = $format. ( lc($name) eq 'zip' ? '-zip' : '' );
       open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
-      $error ||= &{$method}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
+      my $param = { 'filehandle' => $fh,
+                    'format'     => $fmt,
+                    %$addl_param,
+                  };
+      $error ||= &{$method}($param, $job);
       close $fh;
     }
   }
index d2b7013..ee18407 100644 (file)
@@ -494,6 +494,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
@@ -690,5 +692,7 @@ 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
 FS/access_user_session.pm
 t/access_user_session.t
index c37ff11..a7452e8 100755 (executable)
@@ -12,8 +12,8 @@ use FS::cdr;
 # parse command line
 ###
 
-use vars qw( $opt_m $opt_p $opt_r $opt_e $opt_d $opt_v $opt_P $opt_a $opt_c $opt_g );
-getopts('c:m:p:r:e:d:v:P:ag');
+use vars qw( $opt_m $opt_p $opt_r $opt_e $opt_d $opt_v $opt_P $opt_a $opt_c $opt_g $opt_s );
+getopts('c:m:p:r:e:d:v:P:ags');
 
 $opt_e ||= 'csv';
 #$opt_e = ".$opt_e" unless $opt_e =~ /^\./;
@@ -116,31 +116,39 @@ foreach my $filename ( @$ls ) {
   $import_options->{'cdrtypenum'} = $opt_c if $opt_c;
   
   my $error = FS::cdr::batch_import($import_options);
+
   if ( $error ) {
-    unlink "$cachedir/$filename";
-    unlink "$cachedir/$ungziped" if $opt_g;
-    die $error;
-  }
 
-  if ( $opt_d ) {
-    if($opt_m eq 'ftp') {
-      my $ftp = ftp();
-      $ftp->rename($filename, "$opt_d/$file_timestamp")
-        or do {
-          unlink "$cachedir/$filename";
-          unlink "$cachedir/$ungziped" if $opt_g;
-          die "Can't move $filename to $opt_d: ".$ftp->message . "\n";
-        };
+    if ( $opt_s ) {
+      warn "$ungziped: $error\n";
+    } else {
+      unlink "$cachedir/$filename";
+      unlink "$cachedir/$ungziped" if $opt_g;
+      die $error;
     }
-    else {
-      my $sftp = sftp();
-      $sftp->rename($filename, "$opt_d/$file_timestamp")
-        or do {
-          unlink "$cachedir/$filename";
-          unlink "$cachedir/$ungziped" if $opt_g;
-          die "can't move $filename to $opt_d: ". $sftp->error . "\n";
-        };
+
+  } else {
+
+    if ( $opt_d ) {
+      if ( $opt_m eq 'ftp') {
+        my $ftp = ftp();
+        $ftp->rename($filename, "$opt_d/$file_timestamp")
+          or do {
+            unlink "$cachedir/$filename";
+            unlink "$cachedir/$ungziped" if $opt_g;
+            die "Can't move $filename to $opt_d: ".$ftp->message . "\n";
+          };
+      } else {
+        my $sftp = sftp();
+        $sftp->rename($filename, "$opt_d/$file_timestamp")
+          or do {
+            unlink "$cachedir/$filename";
+            unlink "$cachedir/$ungziped" if $opt_g;
+            die "can't move $filename to $opt_d: ". $sftp->error . "\n";
+          };
+      }
     }
+
   }
 
   unlink "$cachedir/$filename";
@@ -192,7 +200,7 @@ freeside-cdr-sftp_and_import - Download CDR files from a remote server via SFTP
 
   cdr.sftp_and_import [ -m method ] [ -p prefix ] [ -e extension ] 
     [ -r remotefolder ] [ -d donefolder ] [ -v level ] [ -P port ]
-    [ -a ] [ -c cdrtypenum ] user format [sftpuser@]servername
+    [ -a ] [ -g ] [ -s ] [ -c cdrtypenum ] user format [sftpuser@]servername
 
 =head1 DESCRIPTION
 
@@ -220,6 +228,8 @@ or FTP and then import them into the database.
 
 -g: File is gzipped
 
+-s: Warn and skip files which could not be imported rather than abort
+
 user: freeside username
 
 format: CDR format name
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 399c119..5bd1415 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/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/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/README b/README
index a2babf3..e68c06f 100644 (file)
--- a/README
+++ b/README
@@ -1,7 +1,7 @@
 Freeside is a billing and administration package for Internet Service 
 Providers, VoIP providers and other online businesses.
 
-Copyright (C) 2005-2011 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
 Additional copyright holders may be found in the docs/license.html file.
diff --git a/bin/3add b/bin/3add
new file mode 100755 (executable)
index 0000000..8bc034d
--- /dev/null
+++ b/bin/3add
@@ -0,0 +1,19 @@
+#!/usr/bin/perl
+
+use Cwd;
+use String::ShellQuote;
+
+my $USER = $ENV{USER};
+
+my $dir = getcwd;
+( my $prefix = $dir ) =~ s(^/home/$USER/freeside/?)() or die $dir; #eventually from anywhere
+
+system join('',
+  "git add @ARGV ; ",
+  "( for file in @ARGV; do ",
+     "cp -i \$file /home/$USER/freeside3/$prefix/`dirname \$file`;",
+  "done ) && ",
+  "cd /home/$USER/freeside3/$prefix/ && ",
+  "git add @ARGV"
+);
+
diff --git a/bin/3commit b/bin/3commit
new file mode 100755 (executable)
index 0000000..cd1db21
--- /dev/null
@@ -0,0 +1,26 @@
+#!/usr/bin/perl
+
+# usage: 23commit 'log message' filename filename ...
+
+use Cwd;
+use String::ShellQuote;
+
+my $USER = $ENV{USER};
+
+my $dir = getcwd;
+( my $prefix = $dir ) =~ s(^/home/$USER/freeside/?)() or die $dir; #eventually from anywhere
+
+my $desc = shell_quote(shift @ARGV); # -m
+
+die "no files!" unless @ARGV;
+
+#warn "$prefix";
+
+#print <<END;
+system join('',
+  "( cd /home/$USER/freeside3/$prefix; git pull ) && ",
+  "git diff -u @ARGV | ( cd /home/$USER/freeside3/$prefix; patch -p1 ) ",
+  " && ( ( git pull && git commit  -m $desc @ARGV && git push ); ",
+  "( cd /home/$USER/freeside3/$prefix; git commit -m $desc @ARGV && git push ) )"
+);
+
diff --git a/bin/3diff b/bin/3diff
new file mode 100755 (executable)
index 0000000..badafd5
--- /dev/null
+++ b/bin/3diff
@@ -0,0 +1,13 @@
+#!/usr/bin/perl
+
+my $file = shift;
+
+chomp(my $dir = `pwd`);
+$dir =~ s/freeside(\/?)/freeside3$1/;
+warn $dir;
+
+#$cmd = "diff -u $file $dir/$file";
+$cmd = "diff -ubBw $dir/$file $file";
+print "$cmd\n";
+system($cmd);
+
diff --git a/bin/cch.finish_failed b/bin/cch.finish_failed
new file mode 100644 (file)
index 0000000..cb25330
--- /dev/null
@@ -0,0 +1,51 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Storable qw( thaw nfreeze );
+use MIME::Base64;
+use FS::UID qw( adminsuidsetup );
+use FS::tax_rate;
+
+adminsuidsetup(shift);
+
+#my @namelist = qw( code detail geocode plus4 txmatrix zip );
+my @namelist = qw( code detail plus4 txmatrix zip );
+
+my $cache_dir = '/usr/local/etc/freeside/cache.'. $FS::UID::datasrc. '/';
+my $dir = $cache_dir.'taxdata/cch';
+
+my @list = ();
+foreach my $name ( @namelist ) {
+  my $difffile = "$dir.new/$name.txt";
+  if (1) { # ($update) {
+    #my $error = $job->update_statustext( "0,Comparing to previous $name" );
+    #die $error if $error;
+    warn "processing $dir.new/$name.txt\n"; # if $DEBUG;
+    #my $olddir = $update ? "$dir.1" : "";
+    my $olddir = "$dir.1";
+    $difffile = FS::tax_rate::_perform_cch_diff( $name, "$dir.new", $olddir );
+  }
+  $difffile =~ s/^$cache_dir//;
+  push @list, "${name}file:$difffile";
+}
+
+# perform the import
+local $FS::tax_rate::keep_cch_files = 1;
+my $param = {
+  'format'         => 'cch-update',
+  'uploaded_files' => join( ',', @list ),
+};
+my $error =
+  #_perform_batch_import( $job, encode_base64( nfreeze( $param ) ) );
+  FS::tax_rate::_perform_batch_import( '', encode_base64( nfreeze( $param ) ) );
+
+if ( $error ) {
+  warn "ERROR: $error\n";
+} else {
+  warn "success!\n";
+}
+
+#XXX do this manually
+#rename "$dir.new", "$dir"
+#  or die "cch tax update processed, but can't rename $dir.new: $!\n";
+
diff --git a/bin/cch.redelete b/bin/cch.redelete
new file mode 100644 (file)
index 0000000..2cff389
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Storable qw( thaw nfreeze );
+use MIME::Base64;
+use FS::UID qw( adminsuidsetup );
+use FS::tax_rate;
+
+adminsuidsetup(shift);
+
+#my @namelist = qw( code detail geocode plus4 txmatrix zip );
+my @namelist = qw( plus4 txmatrix zip );
+
+my $cache_dir = '/usr/local/etc/freeside/cache.'. $FS::UID::datasrc. '/';
+my $dir = $cache_dir.'taxdata/cch';
+
+my @list = ();
+foreach my $name ( @namelist ) {
+  my $difffile = "$dir.new/$name.txt";
+  if (1) { # ($update) {
+    #my $error = $job->update_statustext( "0,Comparing to previous $name" );
+    #die $error if $error;
+    warn "processing $dir.new/$name.txt\n"; # if $DEBUG;
+    #my $olddir = $update ? "$dir.1" : "";
+    my $olddir = "$dir.1";
+    $difffile = FS::tax_rate::_perform_cch_diff( $name, "$dir.new", $olddir );
+  }
+  $difffile =~ s/^$cache_dir//;
+  push @list, "${name}file:$difffile";
+}
+
+# perform the import
+local $FS::tax_rate::keep_cch_files = 1;
+my $param = {
+  'format'         => 'cch-update',
+  'uploaded_files' => join( ',', @list ),
+  'delete_only'    => 1,
+};
+my $error =
+  #_perform_batch_import( $job, encode_base64( nfreeze( $param ) ) );
+  FS::tax_rate::_perform_batch_import( '', encode_base64( nfreeze( $param ) ) );
+
+if ( $error ) {
+  warn "ERROR: $error\n";
+} else {
+  warn "success!\n";
+}
+
+#XXX do this manually
+#rename "$dir.new", "$dir"
+#  or die "cch tax update processed, but can't rename $dir.new: $!\n";
+
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 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 651a8f5..d44f978 100644 (file)
@@ -1799,8 +1799,9 @@ sub domainselector {
            '<INPUT TYPE="hidden" NAME="domsvc" VALUE="'. $key. '"></TD></TR>'
   }
 
-  my $text .= qq!<TR><TD ALIGN="right">Domain</TD><TD><SELECT NAME="domsvc" SIZE=1 STYLE="width: 20em"><OPTION>(Choose Domain)!;
+  my $text .= qq!<TR><TD ALIGN="right">Domain</TD><TD><SELECT NAME="domsvc" SIZE=1 STYLE="width: 20em">!;
 
+  $text .= '<OPTION>(Choose Domain)' unless $domsvc;
 
   foreach my $domain ( sort { $domains->{$a} cmp $domains->{$b} } keys %$domains ) {
     $text .= qq!<OPTION VALUE="!. $domain. '"'.
index b5b9eea..59ee93b 100755 (executable)
@@ -8,7 +8,7 @@ onSubmit="document.OneTrueForm.process.disabled=true">
 <INPUT TYPE="hidden" NAME="session" VALUE="<%=$session_id%>">
 <INPUT TYPE="hidden" NAME="action" VALUE="post_thirdparty_payment">
 <INPUT TYPE="hidden" NAME="payby_method" VALUE="<%= 
-$cgi->param('payby_method') =~ /(CC|ECHECK)/;
+$cgi->param('payby_method') =~ /(CC|ECHECK|PAYPAL)/;
 $1 %>">
 <TABLE BGCOLOR="#cccccc">
 <TR>
index 9ab2622..a6352e0 100644 (file)
@@ -6,18 +6,13 @@ Hello <%= $name %>!<BR><BR>
 <%= include('small_custview') %>
 
 <BR>
-<%= unless ( $access_pkgnum ) {
-      $OUT .= qq!Balance: <B>\$$balance</B><BR><BR>!;
-    }
-    '';
-%>
 
 <%=
   $OUT .= qq! <B><A HREF="${url}invoices">View All Invoices</A></B> &nbsp; &nbsp; !;
 %>
 
 <%= if ( $balance > 0 ) {
-  if (scalar(grep $_, @hide_payment_fields)) {
+  if (scalar(grep $_, @hide_payment_fields)) { # this sucks
     $OUT .= qq! <B><A HREF="${url}make_thirdparty_payment&payby_method=CC">Make a payment</A></B><BR><BR>!;
   } else {
     $OUT .= qq! <B><A HREF="${url}make_payment">Make a payment</A></B><BR>!;
index 4a31b12..cf719e8 100644 (file)
@@ -23,37 +23,44 @@ unless ( $access_pkgnum ) {
       url=>'customer_order_pkg', 'indent'=>2 };
 }
 
+my %payby_mode;
+@payby_mode{@cust_paybys} = @hide_payment_fields;
+# $payby_mode{FOO} is true if FOO is thirdparty, false if it's B::OP,
+# nonexistent if it's not supported
+
 if ( $balance > 0 ) { #XXXFIXME "enable selfservice prepay features" flag or something, eventually per-pkg or something really fancy
 
-  #XXXFIXME still a bit sloppy for multi-gateway of differing namespace
-  my $i = 0;
-  while($i < scalar(@cust_paybys)) { last if $cust_paybys[$i] =~ /^CARD/; $i++ }
-  if ( $cust_paybys[$i] && $cust_paybys[$i] =~ /^CARD/ ) {
+  if ( exists( $payby_mode{CARD} ) ) {
     push @menu, { title  => 'Recharge my account with a credit card',
-                  url    => $hide_payment_fields[$i]
+                  url    => $payby_mode{CARD}
                               ? 'make_thirdparty_payment&payby_method=CC'
                               : 'make_payment',
                   indent => 2,
                  }
   }
 
-  $i = 0;
-  while($i < scalar(@cust_paybys)) { last if $cust_paybys[$i] =~ /^CHEK/; $i++ }
-  if ( $cust_paybys[$i] && $cust_paybys[$i] =~ /^CHEK/ ) {
+  if ( exists( $payby_mode{CHEK} ) ) {
     push @menu, { title  => 'Recharge my account with a check',
-                  url    => $hide_payment_fields[$i]
+                  url    => $payby_mode{CHEK}
                               ? 'make_thirdparty_payment&payby_method=ECHECK'
                               : 'make_ach_payment',
                   indent => 2,
                 }
   }
 
-  push @menu, { title  => 'Recharge my account with a prepaid card',
-                url    => 'recharge_prepay',
-                indent => 2,
-              }
-    if grep(/^PREP/, @cust_paybys);
+  if ( exists( $payby_mode{PREP} ) ) {
+    push @menu, { title  => 'Recharge my account with a prepaid card',
+                  url    => 'recharge_prepay',
+                  indent => 2,
+                }
+  }
 
+  if ( exists( $payby_mode{PPAL} ) ) {
+    push @menu, { title  => 'Recharge my account with PayPal',
+                  url    => 'make_thirdparty_payment&payby_method=PAYPAL',
+                  indent => 2,
+                }
+  }
 }
 
 push @menu,
index f7fe308..40fe98a 100755 (executable)
@@ -667,12 +667,15 @@ sub make_thirdparty_payment {
 }
 
 sub post_thirdparty_payment {
-  $cgi->param('payby_method') =~ /^(CC|ECHECK)$/
+  $cgi->param('payby_method') =~ /^(CC|ECHECK|PAYPAL)$/
     or die "illegal payby method";
   my $method = $1;
   $cgi->param('amount') =~ /^(\d+(\.\d*)?)$/
     or die "illegal amount";
   my $amount = $1;
+  # realtime_collect() returns the result from FS::cust_main->realtime_collect
+  # which returns realtime_bop()
+  # which returns a hashref of popup_url, collectitems, and reference
   my $result = realtime_collect( 
     'session_id' => $session_id,
     'method' => $method, 
index 23d814e..88eab5c 100755 (executable)
@@ -231,7 +231,7 @@ if ( $magic eq 'process' || $action eq 'process_signup' ) {
                 invoicing_list referral_custnum promo_code reg_code
                 override_ban_warn
                 pkgpart refnum agentnum
-                username sec_phrase _password popnum
+                username sec_phrase _password popnum domsvc
                 mac_addr
                 countrycode phonenum sip_password pin prepaid_shortform
               ),
@@ -500,5 +500,7 @@ END
 
 package FS::SelfService::_signupcgi;
 use HTML::Entities;
-use FS::SelfService qw(regionselector expselect popselector didselector);
+use FS::SelfService qw( regionselector expselect popselector domainselector
+                        didselector
+                      );
 
index 6427e6f..a9b6759 100755 (executable)
@@ -33,7 +33,7 @@
 <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 %>">
@@ -45,7 +45,7 @@
 %>
 
 <%=
-  $OUT = join("\n", map { qq|<input type="hidden" name="$_" />| } qw / promo_code reg_code pkgpart username _password _password2 sec_phrase popnum mac_addr countrycode phonenum sip_password pin / );
+  $OUT = join("\n", map { qq|<input type="hidden" name="$_" />| } qw / promo_code reg_code pkgpart username _password _password2 sec_phrase popnum domsvc mac_addr countrycode phonenum sip_password pin / );
 %>
 
 <%=
@@ -214,10 +214,10 @@ 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="">!,
+      '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="">!,
       'LECB' => qq!Phone bill billing<BR>${r}Phone number <INPUT TYPE="text" BANE="LECB_payinfo" VALUE="$payinfo" MAXLENGTH=15 SIZE=16><INPUT TYPE="hidden" NAME="LECB_month" VALUE="12"><INPUT TYPE="hidden" NAME="LECB_year" VALUE="2037"><INPUT TYPE="hidden" NAME="LECB_payname" VALUE="">!,
       'BILL' => qq!Billing<BR>P.O. <INPUT TYPE="text" NAME="BILL_payinfo" VALUE="$payinfo"><BR><INPUT TYPE="hidden" NAME="BILL_month" VALUE="12"><INPUT TYPE="hidden" NAME="BILL_year" VALUE="2037">Attention<INPUT TYPE="text" NAME="BILL_payname" VALUE="$payname">!,
       'COMP' => qq!Complimentary<BR><font color="#ff0000">*</font>Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE="$payinfo"><BR><font color="#ff0000">*</font>Exp !. expselect("COMP", $paydate),
@@ -319,6 +319,12 @@ ENDOUT
   <TD ALIGN="right">Username</TD>
   <TD><INPUT TYPE="text" NAME="username" VALUE="$username"></TD>
 </TR>
+ENDOUT
+
+  $OUT .= domainselector( svcpart=>$default_svcpart, domsvc=>$default_domsvc )
+    if $default_svcpart;
+
+    $OUT .= <<ENDOUT;
 <TR>
   <TD ALIGN="right">Password</TD>
   <TD><INPUT TYPE="password" NAME="_password" VALUE="$_password"></TD>
@@ -439,7 +445,7 @@ function fixup_form() {
     
     var signup_elements = new Array (
       'promo_code', 'reg_code', 'pkgpart',
-      'username', '_password', '_password2', 'sec_phrase', 'popnum',
+      'username', '_password', '_password2', 'sec_phrase', 'popnum', 'domsvc',
       'mac_addr',
       'countrycode', 'phonenum', 'sip_password', 'pin'
     );
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 d9346b8..ff209d2 100755 (executable)
@@ -87,11 +87,14 @@ my $rv = capture_payment(
                      map { $_ => scalar($cgi->param($_)) } $cgi->param
                    },
            url  => $cgi->self_url,
+           cancel => ($cgi->param('cancel') ? 1 : 0),
 );
 
 $error = $rv->{error};
-  
-if ( $error eq '_decline' ) {
+
+if ( $error eq '_cancel' ) {
+  print_okay(%$rv);
+} elsif ( $error eq '_decline' ) {
   print_decline();
 } elsif ( $error ) {
   print_verify();
@@ -133,8 +136,14 @@ sub print_okay {
     $success_url .= '/signup.cgi?action=success';
   }
 
-  print $cgi->header( '-expires' => 'now' ),
-        $success_template->fill_in( HASH => { success_url => $success_url } );
+  if ( $param{error} eq '_cancel' ) {
+    # then the payment was canceled, so don't show a message, just redirect
+    # (during signup, you really need a separate landing page for this case)
+    print $cgi->redirect($success_url);
+  } else {
+    print $cgi->header( '-expires' => 'now' ),
+          $success_template->fill_in( HASH => { success_url => $success_url } );
+  }
 }
 
 sub success_default { #html to use if you don't specify a success file
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 4a7cedf..41a9f3e 100644 (file)
@@ -34,6 +34,7 @@ for <SELECT NAME="cardtype" MULTIPLE>
 %  "Switch",
 %  "Solo",
 %  'ACH',
+%  'PayPal',
 %) { 
 
   <OPTION VALUE="<% $cardtype %>"><% $cardtype || '(Default fallback)' %>
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 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 7baf84d..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',
@@ -80,6 +81,7 @@
                                 size      => 40, #32
                                 maxlength => 50,
                               },
+                              #@locale_fields,
                               {field=>'comment',  type=>'text', size=>40 }, #32
                               { field         => 'agentnum',
                                 type          => 'select-agent',
@@ -337,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;
@@ -368,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 ) = @_;
 
@@ -408,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' }; };
@@ -473,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 {
@@ -487,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 {
@@ -520,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 dfe52f1..7cfab71 100644 (file)
                                   'gateway_action'       => 'Action',
                                   'gateway_options'      => 'Options (Name/Value pairs, <BR>one element per line)',
                                   'gateway_callback_url' => 'Callback URL',
+                                  'gateway_cancel_url'   => 'Cancel URL',
                                 },
           )
 %>
 
 
 <SCRIPT TYPE="text/javascript">
-  var modulesForNamespace = <% to_json(\%modules_for_namespace, {canonical=>1}) %>;
-  function changeNamespace(what) {
-    var ns = what.value;
+  var modulesForNamespace = <% $json->encode(\%modules) %>;
+  function changeNamespace() {
+    var ns = document.getElementById('gateway_namespace').value;
     var select_module = document.getElementById('gateway_module');
     select_module.options.length = 0;
     for (var x in modulesForNamespace[ns]) {
@@ -30,6 +31,7 @@
       select_module.add(o, null);
     }
   }
+  window.onload = changeNamespace;
 </SCRIPT>
 
 <%init>
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
 
-my %modules =  (
-  '2CheckOut'             => 'Business::OnlinePayment',
-  'AuthorizeNet'          => 'Business::OnlinePayment',
-  'BankOfAmerica'         => 'Business::OnlinePayment', #deprecated?
-  'Beanstream'            => 'Business::OnlinePayment',
-  'Capstone'              => 'Business::OnlinePayment',
-  'Cardstream'            => 'Business::OnlinePayment',
-  'CashCow'               => 'Business::OnlinePayment',
-  'CyberSource'           => 'Business::OnlinePayment',
-  'eSec'                  => 'Business::OnlinePayment',
-  'eSelectPlus'           => 'Business::OnlinePayment',
-  'eWayShared'            => 'Business::OnlineThirdPartyPayment',
-  'ElavonVirtualMerchant' => 'Business::OnlinePayment',
-  'Exact'                 => 'Business::OnlinePayment',
-  'iAuthorizer'           => 'Business::OnlinePayment',
-  'Ingotz'                => 'Business::OnlinePayment',
-  'InternetSecure'        => 'Business::OnlinePayment',
-  'Interswitchng'         => 'Business::OnlineThirdPartyPayment',
-  'IPaymentTPG'           => 'Business::OnlinePayment',
-  'IPPay'                 => 'Business::OnlinePayment',
-  'Iridium'               => 'Business::OnlinePayment',
-  'Jettis'                => 'Business::OnlinePayment',
-  'Jety'                  => 'Business::OnlinePayment',
-  'LinkPoint'             => 'Business::OnlinePayment',
-  'MerchantCommerce'      => 'Business::OnlinePayment',
-  'Network1Financial'     => 'Business::OnlinePayment',
-  'OCV'                   => 'Business::OnlinePayment',
-  'OpenECHO'              => 'Business::OnlinePayment',
-  'PayConnect'            => 'Business::OnlinePayment',
-  'PayflowPro'            => 'Business::OnlinePayment',
-  'PaymenTech'            => 'Business::OnlinePayment',
-  'PaymentsGateway'       => 'Business::OnlinePayment',
-  'PayPal'                => 'Business::OnlinePayment',
-  #'PaySystems'            => 'Business::OnlinePayment',
-  'PlugnPay'              => 'Business::OnlinePayment',
-  'PPIPayMover'           => 'Business::OnlinePayment',
-  'Protx'                 => 'Business::OnlinePayment', #now SagePay
-  'PXPost'                => 'Business::OnlinePayment',
-  'SagePay'               => 'Business::OnlinePayment',
-  'SecureHostingUPG'      => 'Business::OnlinePayment',
-  'Skipjack'              => 'Business::OnlinePayment',
-  'StGeorge'              => 'Business::OnlinePayment',
-  'SurePay'               => 'Business::OnlinePayment',
-  'TCLink'                => 'Business::OnlinePayment',
-  'TransactionCentral'    => 'Business::OnlinePayment',
-  'TransFirsteLink'       => 'Business::OnlinePayment',
-  'Vanco'                 => 'Business::OnlinePayment',
-  'viaKLIX'               => 'Business::OnlinePayment',
-  'VirtualNet'            => 'Business::OnlinePayment',
-  'WesternACH'            => 'Business::OnlinePayment',
-  'WorldPay'              => 'Business::OnlinePayment',
-
-  'KeyBank'               => 'Business::BatchPayment',
-  'Paymentech'            => 'Business::BatchPayment',
-  'TD_EFT'                => 'Business::BatchPayment',
+my $json = JSON::XS->new;
+$json->canonical(1);
+my %modules = (
+  'Business::OnlinePayment' => [
+    '2CheckOut',
+    'AuthorizeNet',
+    'BankOfAmerica', #deprecated?
+    'Beanstream',
+    'Capstone',
+    'Cardstream',
+    'CashCow',
+    'CyberSource',
+    'eSec',
+    'eSelectPlus',
+    'ElavonVirtualMerchant',
+    'Exact',
+    'iAuthorizer',
+    'Ingotz',
+    'InternetSecure',
+    'IPaymentTPG',
+    'IPPay',
+    'Iridium',
+    'Jettis',
+    'Jety',
+    'LinkPoint',
+    'MerchantCommerce',
+    'Network1Financial',
+    'OCV',
+    'OpenECHO',
+    'PayConnect',
+    'PayflowPro',
+    'PaymenTech',
+    'PaymentsGateway',
+    'PayPal',
+    #'PaySystems',
+    'PlugnPay',
+    'PPIPayMover',
+    'Protx', #now SagePay
+    'PXPost',
+    'SagePay',
+    'SecureHostingUPG',
+    'Skipjack',
+    'StGeorge',
+    'SurePay',
+    'TCLink',
+    'TransactionCentral',
+    'TransFirsteLink',
+    'Vanco',
+    'viaKLIX',
+    'VirtualNet',
+    'WesternACH',
+    'WorldPay',
+  ],
+  'Business::OnlineThirdPartyPayment' => [
+    'eWayShared',
+    'Interswitchng',
+    'PayPal',
+  ],
+  'Business::BatchPayment' => [
+    'KeyBank',
+    'Paymentech',
+    'TD_EFT',
+  ],
 );
 
-my %modules_for_namespace;
-for (keys %modules) {
-  $modules_for_namespace{$modules{$_}} ||= [];
-  push @{ $modules_for_namespace{$modules{$_}} }, $_;
-}
-
 my @actions = (
                 'Normal Authorization',
                 'Authorization Only',
@@ -125,7 +129,9 @@ my $fields = [
                {
                  field    => 'gateway_module',
                  type     => 'select',
-                 options  => [ sort { lc($a) cmp lc ($b) } keys %modules ],
+                 # does it even make sense to list all modules here?
+                 options  => [ sort { lc($a) cmp lc ($b) }
+                               map { @$_ } values %modules ],
                },
                'gateway_username',
                'gateway_password',
@@ -140,6 +146,11 @@ my $fields = [
                  size     => 40,
                },
                {
+                 field    => 'gateway_cancel_url',
+                 type     => 'text',
+                 size     => 40,
+               },
+               {
                  field               => 'gateway_options',
                  type                => 'textarea',
                  rows                => '12',
index 2770f32..c893f13 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({
     'custnum' => $cust_pkg->custnum,
     map { $_ => scalar($cgi->param($_)) }
         qw( address1 address2 city county state zip country )
-  };
+  });
   $change{'cust_location'} = $cust_location;
 }
 
index b9f93db..fd1b874 100644 (file)
@@ -31,10 +31,9 @@ die "unknown locationnum $locationnum" unless $cust_location;
 my $new = FS::cust_location->new({
   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);
+my $error = $new->find_or_insert;
+$error  ||= $cust_location->move_to($new);
 
 </%init>
index 054973f..d295ed3 100755 (executable)
@@ -11,7 +11,7 @@
 <%once>
 
 my $me = '[edit/process/cust_main.cgi]';
-my $DEBUG = 0;
+my $DEBUG = 1;
 
 </%once>
 <%init>
@@ -83,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(\%hash);
 }
 
 if ( ($cgi->param('same') || '') eq 'Y' ) {
diff --git a/httemplate/edit/process/detach-cust_pkg.html b/httemplate/edit/process/detach-cust_pkg.html
new file mode 100644 (file)
index 0000000..ab87eb5
--- /dev/null
@@ -0,0 +1,47 @@
+% if ($error) {
+%   $cgi->param('error', $error);
+%   $cgi->redirect(popurl(3). 'misc/detach_pkg.html?'. $cgi->query_string );
+% } else {
+
+    <% header(emt("Package detached")) %>
+      <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;
+
+my $cust_location = new FS::cust_location {
+  map { $_ => scalar($cgi->param($_)) } FS::cust_main->location_fields
+};
+
+my $cust_main = new FS::cust_main {
+  ( map { ( $_, scalar($cgi->param($_)) ) } fields('cust_main') ),
+  ( map { ( "ship_$_", '' ) } FS::cust_main->location_fields ),
+  'bill_location' => $cust_location,
+  'ship_location' => $cust_location,
+};
+
+my $pkg_or_error = $cust_pkg->change( {
+  'keep_dates' => 1,
+  'cust_main'  => $cust_main,
+} );
+
+my $error = ref($pkg_or_error) ? '' : $pkg_or_error;
+
+</%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 2ac57f9..932e33b 100755 (executable)
@@ -10,6 +10,7 @@
               'precheck_callback' => $precheck_callback,
               'args_callback'     => $args_callback,
               'process_m2m'       => \@process_m2m,
+              'process_o2m'       => \@process_o2m,
           )
 %>
 <%init>
@@ -244,4 +245,11 @@ if ( $cgi->param('pkgpart') || ! $conf->exists('agent_defaultpkg') ) {
   };
 }
 
+my @process_o2m = (
+  {
+    'table'  => 'part_pkg_msgcat',
+    'fields' => [qw( locale pkg )],
+  },
+);
+
 </%init>
index 812c988..157449e 100644 (file)
@@ -15,6 +15,7 @@ my $args_callback = sub {
   my @options = split(/\r?\n/, $cgi->param('gateway_options') );
   pop @options
     if scalar(@options) % 2 && $options[-1] =~ /^\s*$/;
+  @options = ( {} ) if !@options;
   (@options)
 };
 
index 2dadbcc..14dbda1 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({
       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 27e9755..09398fd 100644 (file)
@@ -40,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({
       map { $_ => scalar($cgi->param($_)) }
           qw( custnum address1 address2 city county state zip country )
-    };
+    });
     $opt{'cust_location'} = $cust_location;
   }
 
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 3a3bd40..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;
@@ -190,7 +190,7 @@ function <%$pre%>init() {
   <%$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 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 14d36c3..5689b12 100644 (file)
@@ -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',
@@ -397,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' ]
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 e332eef..48469dc 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;
 
@@ -181,6 +174,8 @@ for( $i = 0; $i < @$tiers; $i++ ) {
         $children_of{$key}->{''} = $tier->{empty_label};
       }
     }
+    # ensure that there's always at least one empty label
+    $children_of{''}->{''} = $tier->{empty_label};
   }
   $tier->{by_key} = \%children_of;
 }
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,
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 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 ef06441..0b2f1f1 100644 (file)
@@ -23,10 +23,12 @@ 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);
 % }
@@ -34,9 +36,6 @@ function custnum_update_callback(rownum, prefix) {
 
 function invnum_update_callback(rownum, prefix) {
   custnum_update_callback(rownum, prefix);
-  var enable = document.getElementById('enable_app'+rownum);
-  enable.checked = true;
-  toggle_application_row.call(enable);
 }
 
 function select_discount_term(row, prefix) {
@@ -96,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);
   }
 }
 
index 4919c66..4b3084f 100755 (executable)
@@ -15,19 +15,32 @@ my($query) = $cgi->keywords;
 $query =~ /^(\d+)$/;
 my $svcnum = $1;
 
-#my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$svcnum});
-#die "Unknown svcnum!" unless $svc_acct;
-
+my $error = '';
 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$svcnum});
-die "Unknown svcnum!" unless $cust_svc;
-my $cust_pkg = $cust_svc->cust_pkg;
-if ( $cust_pkg ) {
-  errorpage( 'This account has already been audited.  Cancel the '.
-           qq!<A HREF="${p}view/cust_main.cgi?!. $cust_pkg->custnum.
-           '#cust_pkg'. $cust_pkg->pkgnum. '">'.
-           'package</A> instead.');
-}
+if ( $cust_svc ) {
+  my $cust_pkg = $cust_svc->cust_pkg;
+  if ( $cust_pkg ) {
+    errorpage( 'This account has already been audited.  Cancel the '.
+             qq!<A HREF="${p}view/cust_main.cgi?!. $cust_pkg->custnum.
+             '#cust_pkg'. $cust_pkg->pkgnum. '">'.
+             'package</A> instead.'); #'
+  }
 
-my $error = $cust_svc->cancel;
+  $error = $cust_svc->cancel;
+} else {
+  # the rare obscure case: svc_x without cust_svc
+  my $svc_x;
+  foreach my $svcdb (FS::part_svc->svc_tables) {
+    $svc_x = qsearchs($svcdb, { 'svcnum' => $svcnum });
+    last if $svc_x;
+  }
+  if ( $svc_x ) {
+    $error = $svc_x->return_inventory
+             || $svc_x->FS::Record::delete;
+  } else {
+    # the svcnum really doesn't exist
+    $error = "svcnum $svcnum not found";
+  }
+}
 
 </%init>
diff --git a/httemplate/misc/change_pkg_contact.html b/httemplate/misc/change_pkg_contact.html
new file mode 100755 (executable)
index 0000000..c88140e
--- /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 BGCOLOR="#dddddd">
+      <% $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 BGCOLOR="#dddddd">
+        <% $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 6ef7623..23099c4 100644 (file)
@@ -11,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 a277ba4..43b9229 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson( \@return ) %>
+<% encode_json( \@return ) %>\
 <%init>
 
 my( $custnum, $prospectnum, $classnum ) = $cgi->param('arg');
index 4decbef..3b4425f 100755 (executable)
@@ -31,7 +31,17 @@ if ( $cgi->param('new_custnum') =~ /^(\d+)$/ ) {
   } );
   die "No customer # $custnum" unless $cust_main;
 
-  $error = $cust_main->merge($new_custnum);
+  if ( $cgi->param('merge') eq 'Y' ) {
+
+    #old-style merge: everything + delete old customer
+    $error = $cust_main->merge($new_custnum);
+
+  } else {
+
+    #new-style attach: move packages 3.0 style, that's it
+    $error = $cust_main->attach_pkgs($new_custnum);
+
+  }
 
 } else {
   $error = 'Select a customer to merge into';
diff --git a/httemplate/misc/delete-note.html b/httemplate/misc/delete-note.html
new file mode 100644 (file)
index 0000000..436326f
--- /dev/null
@@ -0,0 +1,11 @@
+<%init>
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Edit customer note');
+
+my ($notenum) = $cgi->keywords;
+$notenum =~ /^\d+$/ or die "bad notenum '$notenum'";
+my $note = FS::cust_main_note->by_key($notenum)
+  or die "notenum '$notenum' not found";
+$note->delete;
+</%init>
+<% $cgi->redirect($p.'view/cust_main.cgi?'.$note->custnum) %>
diff --git a/httemplate/misc/detach_pkg.html b/httemplate/misc/detach_pkg.html
new file mode 100755 (executable)
index 0000000..64b3e6e
--- /dev/null
@@ -0,0 +1,104 @@
+<& /elements/header-popup.html, mt("Detach Package to New Customer") &>
+
+<SCRIPT TYPE="text/javascript" SRC="../elements/order_pkg.js"></SCRIPT>
+
+<& /elements/error.html &>
+
+<FORM NAME="OrderPkgForm" ACTION="<% $p %>edit/process/detach-cust_pkg.html" METHOD=POST>
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
+% foreach my $f (qw( agentnum refnum )) {
+  <INPUT TYPE="hidden" NAME="<% $f %>" VALUE="<% $cust_main->$f() %>">
+% }
+<INPUT TYPE="hidden" NAME="referral_custnum" VALUE="<% $cust_main->custnum %>">
+% foreach my $f (FS::cust_main->location_fields) {
+  <INPUT TYPE="hidden" NAME="<% $f %>" VALUE="<% $loc->$f() |h %>">
+% }
+
+<% ntable('#cccccc') %>
+
+  <TR>
+    <TH ALIGN="right"><% mt('Package') |h %></TH>
+    <TD COLSPAN=7 BGCOLOR="#dddddd">
+      <% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><B><% $part_pkg->pkg |h %></B> - <% $part_pkg->comment |h %>
+    </TD>
+  </TR>
+
+% #always should be present for detaching, yes? #if ( $cust_pkg->contactnum ) {
+%   my $cust_contact = $cust_pkg->contact_obj;
+
+    <INPUT TYPE="hidden" NAME="first" VALUE="<% $cust_contact->get('first') |h %>">
+    <INPUT TYPE="hidden" NAME="last"  VALUE="<% $cust_contact->get('last')  |h %>">
+
+    <TR>
+      <TH ALIGN="right"><% mt('Name') %></TH>
+      <TD COLSPAN=7 BGCOLOR="#dddddd">
+        <% $cust_pkg->contact_obj->line |h %>
+      </TD>
+    </TR>
+% #}
+
+  <TR>
+    <TH ALIGN="right" VALIGN="top"><% mt('Address') %></TH>
+    <TD COLSPAN=7 BGCOLOR="#dddddd">
+
+      <% $loc->location_label( 'join_string'     => '<BR>',
+                               'double_space'    => ' &nbsp; ',
+                               'escape_function' => \&encode_entities,
+                               'countrydefault'  => $countrydefault,
+                             )
+      %>
+    </TD>
+  </TR>
+
+</TABLE>
+
+%#XXX payment info
+%#XXX should be sticky on errors...
+<& /edit/cust_main/billing.html, FS::cust_main->new({}),
+                                 invoicing_list => [],
+
+&>
+
+<BR>
+<BR>
+<INPUT NAME    = "submitButton"
+       TYPE    = "submit"
+       VALUE   = "<% mt("Detach package") |h %>"
+>
+
+%#and a cancel button?  or is the popup close sufficient?
+
+</FORM>
+</BODY>
+</HTML>
+
+<%init>
+
+my $conf = new FS::Conf;
+my $countrydefault = $conf->config('countrydefault') || 'US';
+
+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 $loc = $cust_pkg->cust_location_or_main;
+
+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 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 7ed5c66..cec0e31 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson(\@macs) %>
+<% encode_json(\@macs) %>\
 <%init>
 
 # XXX: this should be agent-virtualized / limited
index 8acae2b..a872d49 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson( $return ) %>
+<% encode_json( $return ) %>\
 <%init>
 
 my $return;
index ad075be..9c869fa 100644 (file)
@@ -1,6 +1,6 @@
-<% include('/elements/header-popup.html', 'Merge customer' ) %>
+<& /elements/header-popup.html, 'Merge customer' &>
 
-<% include('/elements/error.html') %>
+<& /elements/error.html &>
 
 <FORM NAME="cust_merge_popup" ID="cust_merge_popup" ACTION="<% popurl(1) %>cust_main-merge.html" METHOD=POST onSubmit="submit_merge(); return false;">
 
@@ -35,13 +35,43 @@ function do_submit_merge() {
 <INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
 
 <TABLE BORDER="0" CELLSPACING="2" STYLE="margin-left:auto; margin-right:auto">
-  <% include('/elements/tr-search-cust_main.html',
+
+  <& /elements/tr-search-cust_main.html,
                'label'       => 'Merge into: ',
                'field'       => 'new_custnum',
                'find_button' => 1,
                'curr_value'  => scalar($cgi->param('new_custnum')),
-            )
-  %>
+  &>
+
+% if ( $conf->exists('deletecustomers') ) {
+
+%   if ( scalar($cust_main->ncancelled_pkgs) ) {
+      <TR>
+        <TD COLSPAN=2>
+          <& /elements/radio.html,
+               'field'       => 'merge',
+               'value'       => '',
+               'curr_value'  => scalar($cgi->param('merge')),
+          &>
+          Merge packages only.
+        </TD>
+      </TR>
+%   } else {
+%     $cgi->param('merge', 'Y');
+%   }
+
+    <TR>
+      <TD COLSPAN=2>
+        <& /elements/radio.html,
+             'field'       => 'merge',
+             'value'       => 'Y',
+             'curr_value'  => scalar($cgi->param('merge')),
+        &>
+        Merge invoices, payments/credits, notes, tickets and delete this customer.
+      </TD>
+    </TR>
+% }
+
 </TABLE>
 
 <P ALIGN="CENTER">
@@ -54,6 +84,8 @@ function do_submit_merge() {
 
 <%init>
 
+my $conf = new FS::Conf;
+
 $cgi->param('custnum') =~ /^(\d+)$/ or die 'illegal custnum';
 my $custnum = $1;
 
index 993ea36..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"
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_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>
index 2450ea3..31538b0 100644 (file)
@@ -1,4 +1,4 @@
-<% objToJson(\@regions) %>
+<% encode_json(\@regions) %>\
 <%init>
 
 my( $state, $svcpart ) = $cgi->param('arg');
index 15f266a..6182653 100644 (file)
@@ -1,4 +1,4 @@
-<% encode_json($return) %>
+<% encode_json($return) %>\
 <%init>
 
 local $SIG{__DIE__}; #disable Mason error trap
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 f618d55..c0db3e2 100644 (file)
@@ -1,4 +1,4 @@
-<% to_json($return) %>
+<% encode_json($return) %>\
 <%init>
 
 my $curuser = $FS::CurrentUser::CurrentUser;
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>
 
index d8c8ef4..0d83082 100644 (file)
@@ -1,4 +1,4 @@
-<% JSON::to_json(\@result) %>\
+<% encode_json(\@result) %>\
 <%init>
 die 'access denied'
   unless $FS::CurrentUser::CurrentUser->access_right('Edit customer');
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 04764c1..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) %>>
 %         }
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 666032d..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-<& 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 ebf081c..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-<& 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 6a53229..95c00a3 100755 (executable)
@@ -1,17 +1,44 @@
-<& 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 2106a44..b2dd9ca 100755 (executable)
@@ -1,3 +1,6 @@
+% if ( $cgi->param('_type') =~ /^xml$/ ) {
+<zip_code>
+% }
 <& elements/search.html,
                   'html_init'         => $html_init,
                   'name'              => 'zip code',
@@ -14,6 +17,9 @@
 
               
 &>
+% if ( $cgi->param('_type') =~ /^xml$/ ) {
+</zip_code>
+% }
 <%init>
 
 my $curuser = $FS::CurrentUser::CurrentUser;
index b8fbe20..b94ae9f 100644 (file)
@@ -1,6 +1,12 @@
 %# 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 {
@@ -58,6 +64,7 @@ td.money:before { content: '<% $money_char %>'; }
   </TR>
 </TABLE>
 <& /elements/footer.html &>
+% }
 <%init>
 die "access denied" 
   unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
@@ -100,10 +107,91 @@ my @cust_pkg = qsearch($query);
 
 my $money_char = FS::Conf->new->config('money_char') || '$';
 
-#my $count_query = 
-#  'SELECT COUNT(*) FROM cust_pkg '.$query->{'addl_from'}.$query->{'extra_sql'}.
-#  ' AND EXISTS(SELECT 1 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 $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 88cdaf5..473aed3 100755 (executable)
@@ -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') ];
   }
 
index 0b64e65..ff20458 100644 (file)
@@ -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;
 }
 
index 0f51d94..3a3b0fe 100644 (file)
@@ -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";
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 d1f41df..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;
 }
 
index 1504f0f..8174200 100644 (file)
@@ -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;
 }
 
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 2afce0c..54c9935 100755 (executable)
@@ -1,4 +1,4 @@
-<% include( 'elements/cust_pay_or_refund.html',
+<& elements/cust_pay_or_refund.html,
                 'thing'         => 'pay_pending',
                 'amount_field'  => 'paid',
                 'name_singular' => 'pending payment',
@@ -10,8 +10,7 @@
                                      $status_sub,
                                    ],
                 'redirect_empty' => $redirect_empty,
-          )
-%>
+&>
 <%init>
 
 my %statusaction = (
index e2a83b7..b245d31 100644 (file)
@@ -96,6 +96,7 @@ if ( length( $cgi->param('search_svc') ) ) {
   my $extra_sql = ' WHERE '. join(' AND ', @extra_sql );
 
   $sql_query = {
+    'select'     => 'svcnum',
     'table'      => 'cust_svc',
     'addl_from'  => $addl_from,
     'hashref'    => {},
@@ -105,8 +106,8 @@ if ( length( $cgi->param('search_svc') ) ) {
 }
 
 $sql_query->{'select'} = join(', ',
-                                    'cust_svc.*',
-                                    'part_svc.*',
+                                    $sql_query->{'select'},
+                                    #'part_svc.*',
                                     'cust_main.custnum',
                                     FS::UI::Web::cust_sql_fields(),
                              );
@@ -117,14 +118,17 @@ my $count_query = "SELECT COUNT(*) FROM cust_svc ". $sql_query->{addl_from}.
 
 my $link = sub {
   my $cust_svc = shift;
-  my $url = svc_url(
-    'm'        => $m,
-    'action'   => 'view',
-    #'part_svc' => $cust_svc->part_svc,
-    'svcdb'    => $cust_svc->svcdb, #we have it from the joined search
-    #'svc'      => $cust_svc, #redundant
-    'query'     => '',
-  );
+  my $url;
+  if ( $cust_svc->svcpart ) {
+    $url = svc_url(
+      'm'        => $m,
+      'action'   => 'view',
+      'svcdb'    => $cust_svc->svcdb, #we have it from the joined search
+      'query'     => '',
+    );
+  } else { # bizarre unlinked service case
+    $url = $p.'view/svc_Common.html?svcdb='.$cust_svc->svcdb.';svcnum=';
+  }
   [ $url, 'svcnum' ];
 };
 
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);
index c9c71f2..4933652 100644 (file)
@@ -2,10 +2,10 @@
 
 Example:
 
-  include( 'elements/cust_main_dayranges.html',
+  <& elements/cust_main_dayranges.html,
                  'title'       => 'Accounts Receivable Aging Summary',
                  'range_sub'   => $mysub,
-         )
+  &>
 
   my $mysub = sub {
     my( $start, $end ) = @_;
@@ -44,7 +44,7 @@ Example:
                                              $row->{'rangecol_60_90'} ),
                                     sprintf( $money_char.'%.2f',
                                              $row->{'rangecol_90_0'} ),
-                                    sprintf( '<b>'. $money_char.'%.2f'. '</b>',
+                                    sprintf( '<b>'.$money_char.'%.2f</b>',
                                              $row->{'rangecol_0_0'} ),
                                     ('') x @pay_labels,
                                   ],
@@ -81,6 +81,9 @@ Example:
                                     '', '', '', '', 'b', 
                                     ( map '', @pay_labels ),
                                     ],
+                 'xls_format'  => [ (map '', FS::UI::Web::cust_styles),
+                                    '', '', '', '', { bold => 1 },
+                                  ],
                  'color'       => [
                                     FS::UI::Web::cust_colors(),
                                     '',
@@ -162,6 +165,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;
 
index 3e5d504..7b2a170 100755 (executable)
@@ -252,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;
     }
 
index 26a51c4..bc844a5 100644 (file)
@@ -6,6 +6,8 @@ my $header = $args{'header'};
 my $rows   = $args{'rows'};
 my %opt    = %{ $args{'opt'} };    
 
+my $style  = $opt{'style'};
+
 my $override = scalar(@$rows) >= 65536 ? 'XLSX' : '';
 
 my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format($override);
@@ -42,6 +44,12 @@ my $header_format = $workbook->add_format(
   bg_color => 55, #22,
   bottom   => 3,
 );
+my $footer_format = $workbook->add_format(
+  italic   => 1,
+  locked   => 1,
+  bg_color => 55,
+  top      => 3,
+);
 my $default_format = $workbook->add_format(locked => 0);
 
 my %money_format;
@@ -50,10 +58,24 @@ my $money_char = FS::Conf->new->config('money_char') || '$';
 my %date_format;
 xl_parse_date_init();
 
+my %bold_format;
+
 my $writer = sub {
   # Wrapper for $worksheet->write.
   # Do any massaging of the value/format here.
   my ($r, $c, $value, $format) = @_;
+  #warn "writer called with format $format\n";
+
+  if ( $style->[$c] eq 'b' or $value =~ /<b>/i ) { # the only one in common use
+    $value =~ s[</?b>][]ig;
+    if ( !exists($bold_format{$format}) ) {
+      $bold_format{$format} = $workbook->add_format();
+      $bold_format{$format}->copy($format);
+      $bold_format{$format}->set_bold();
+    }
+    $format = $bold_format{$format};
+  }
+
   # convert HTML entities
   # both Spreadsheet::WriteExcel and Excel::Writer::XLSX accept UTF-8 strings
   $value = decode_entities($value);
@@ -86,6 +108,7 @@ my $writer = sub {
     # String: replace line breaks with newlines
     $value =~ s/<BR>/\n/gi;
   }
+  #warn "writing with format $format\n";
   $worksheet->write($r, $c, $value, $format);
 };
 
@@ -140,7 +163,7 @@ if ( $opt{'footer'} ) {
     if ( ref($item) eq 'CODE' ) {
       $item = &{$item}();
     }
-    $writer->( $r, $c++, $item, $header_format );
+    $writer->( $r, $c++, $item, $footer_format );
   }
 }
 
index 68c4888..d44b454 100644 (file)
@@ -353,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} ) =~
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 99330fa..6d2dd99 100755 (executable)
@@ -1,9 +1,8 @@
-<% include( 'elements/cust_pay_or_refund.html',
+<& elements/cust_pay_or_refund.html,
                 'table'         => 'h_cust_pay',
                 'amount_field'  => 'paid',
                 'name_singular' => 'payment',
                 'name_verb'     => 'paid',
                 'pre_header'    => [ 'Transaction',    'By' ],
                 'pre_fields'    => [ 'history_action', 'history_user' ],
-          )
-%>
+&>
index 2178346..a90f13c 100644 (file)
@@ -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 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 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 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 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 e232291..f5c2bf0 100755 (executable)
@@ -1,9 +1,8 @@
-<% include( 'elements/cust_main_dayranges.html',
+<& elements/cust_main_dayranges.html,
                  #'title'       => 'Prepaid Balance Aging Summary', #???
                  'title'       => 'Unapplied Payments Aging Summary',
                  'range_sub'   => \&unapplied_payments,
-          )
-%>
+&>
 <%init>
 
 die "access denied"
index 425aa5a..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;
 }
 
index ec31919..be0100f 100755 (executable)
@@ -91,14 +91,19 @@ function areyousure(href, message) {
   &> | 
 % }
 
-% if ( $curuser->access_right('Merge customer') ) {
+% if (     $curuser->access_right('Merge customer')
+%      and (    scalar($cust_main->ncancelled_pkgs)
+%            || $conf->exists('deletecustomers')
+%          )
+%    )
+% {
   <& /elements/popup_link-cust_main.html,
               { 'action'      => $p. 'misc/merge_cust.html',
                 'label'       => emt('Merge this customer'),
                 'actionlabel' => emt('Merge customer'),
                 'cust_main'   => $cust_main,
-                'width'       => 480,
-                'height'      => 192,
+                'width'       => 569,
+                'height'      => 210,
               }
   &> | 
 % } 
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 b29d0ce..689c9a3 100755 (executable)
@@ -36,7 +36,7 @@ STYLE="padding-bottom: 0px;
 % }
 </SPAN></TH></TR>
 %   if (@$packages) {
-<& packages/section.html, 'packages' => $packages &>
+<& packages/section.html, 'packages' => $packages, 'cust_main' => $cust_main &>
 %   }
 </TABLE><BR>
 % } #foreach $locationnum
index 1e9f464..2de68ff 100755 (executable)
 %
 %   my $edit = '';
 %   if ($curuser->access_right('Edit customer note') ) {
-%     $edit = qq! <A HREF="javascript:void(0);" $clickjs>(!.emt('edit').')</A>';
+%     my $delete_url = $fsurl.'misc/delete-note.html?'.$notenum;
+%     $edit = qq! <A HREF="javascript:void(0);" $clickjs>(!.emt('edit').')</A>'.
+%             qq! <A HREF="$delete_url" !.
+%             qq! onclick="return confirm('Delete this note?')">!.
+%             '('.emt('delete').')</A>';
 %   }
 %
 % if ( $last_classnum != $note->classnum && !$skipheader ) {
index 24a12cc..546dd89 100755 (executable)
@@ -101,7 +101,7 @@ table.usage {
 
   <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,
@@ -113,7 +113,6 @@ table.usage {
 <& packages/section.html,
     'cust_main'     => $cust_main,
     'packages'      => $packages,
-    'show_location' => $show_location,
  &>
 </TABLE>
 % }
@@ -140,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 ne $cust_main->ship_locationnum, @$packages);
-
 my $countrydefault = scalar($conf->config('countrydefault')) || 'US';
 #subroutines
 
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..fe8b715
--- /dev/null
@@ -0,0 +1,87 @@
+% if ( $contact ) {
+    <% $contact->line |h %>
+% if ( $show_change_link ) {
+        <FONT SIZE=-1>
+          (&nbsp;<%pkg_change_contact_link($cust_pkg)%>&nbsp;)
+        </FONT>
+% }
+% if ( $show_detach_link ) {
+        <FONT SIZE=-1>
+          (&nbsp;<%pkg_detach_link($cust_pkg)%>&nbsp;)
+        </FONT>
+%    }
+% } elsif ( $show_contact_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_change_link = 
+  ! $cust_pkg->get('cancel')
+  && $FS::CurrentUser::CurrentUser->access_right('Change customer package');
+
+my $show_detach_link =
+  ! $cust_pkg->get('cancel')
+  && $FS::CurrentUser::CurrentUser->access_right('Detach customer package');
+
+my $show_contact_link =
+  ! $cust_pkg->get('cancel')
+  ; #&& $FS::CurrentUser::CurrentUser->access_right('Add package contact'); #or something like that
+
+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('Add contact'),
+    'cust_pkg'    => $cust_pkg,
+    'width'       => 616,
+    'height'      => 192,
+  );
+}
+
+sub pkg_detach_link {
+  my $cust_pkg = shift;
+  #my $pkgpart = $cust_pkg->pkgpart;
+  include( '/elements/popup_link-cust_pkg.html',
+    'action'      => $p. "misc/detach_pkg.html",
+    'label'       => emt('Detach'),
+    'actionlabel' => emt('Detach'),
+    'cust_pkg'    => $cust_pkg,
+    'width'       => 616,
+    'height'      => 684,
+  );
+}
+
+#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 0b72d19..520305a 100644 (file)
 %           !$supplemental and
 %           $part_pkg->freq ne '0' ) {
       <TR>
-%       if ( !$opt{'show_location'} ) {
-        <TD><FONT SIZE="-1">
-          (&nbsp;<% pkg_change_location_link($cust_pkg) %>&nbsp;)
-        </FONT></TD>
-%       }
 %       if ( FS::Conf->new->exists('invoice-unitprice') ) {
         <TD><FONT SIZE="-1">
           (&nbsp;<% pkg_change_quantity_link($cust_pkg) %>&nbsp;)
index 5224619..391a13b 100755 (executable)
@@ -3,9 +3,7 @@
 % #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>
 
@@ -13,6 +11,7 @@
 %   foreach my $cust_pkg (@$packages) {
     <& .packagerow, $cust_pkg,
         'cust_main' => $opt{'cust_main'},
+        'bgcolor'   => $opt{'bgcolor'},
         %conf_opt
     &>
 %   }
   <!--pkgnum: <% $cust_pkg->pkgnum %>-->
   <TR CLASS="row<%$row % 2%>">
     <& package.html, %iopt &>
-    <& status.html, %iopt &>
-%     if ( $iopt{'show_location'} ) {
-    <& location.html, %iopt &>
-%     }
+    <& status.html,  %iopt &>
+    <TD CLASS="inv" BGCOLOR="<% $iopt{bgcolor} %>" WIDTH="20%" VALIGN="top">
+      <& contact.html, %iopt &><BR>
+      <& location.html, %iopt &>
+    </TD>
     <& services.html, %iopt &>
   </TR>
 % $row++;
@@ -51,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 { 
@@ -60,6 +59,15 @@ my $show_location = $opt{'show_location'};
   ( $a->getfield('pkgnum') <=> $b->getfield('pkgnum') )
 } @$packages;
 
+my %change_custnum = map { $_->change_custnum => 1 }
+                       grep { $_->change_custnum }
+                         grep { $_->getfield('cancel') }
+                           @$packages;
+
+my $pkg_attached = ( scalar(keys %change_custnum) == 1
+                       && ! grep { ! $_->getfield('cancel') } @$packages
+                   );
+
 my $countrydefault = scalar($conf->config('countrydefault')) || 'US';  
 
 my %conf_opt = (
@@ -68,6 +76,7 @@ my %conf_opt = (
                                  || $curuser->option('cust_pkg-display_times')),
   #for status.html
   'cust_pkg-show_autosuspend' => $conf->exists('cust_pkg-show_autosuspend'),
+  'pkg_attached'              => $pkg_attached,
   #for status.html pkg-balances
   'pkg-balances'              => $conf->exists('pkg-balances'),
   'money_char'                => ( $conf->config('money_char') || '$' ),
@@ -84,10 +93,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 6be0296..c0213e9 100644 (file)
@@ -1,4 +1,4 @@
-<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
@@ -14,6 +14,8 @@
 
     <% pkg_status_row($cust_pkg, emt('Cancelled'), 'cancel', 'color'=>'FF0000', %opt ) %>
 
+    <% pkg_status_row_detached($cust_pkg, %opt) %>
+
     <% pkg_reason_row($cust_pkg, $cpr, color => 'ff0000', %opt) %>
 
 %   unless ( $cust_pkg->get('setup') ) { 
@@ -29,7 +31,7 @@
 
 %   } 
 %
-%   if ( $part_pkg->freq and !$supplemental ) { #?
+%   if ( $part_pkg->freq && !$supplemental && !$cust_pkg->change_custnum ) { #?
 
       <TR>
         <TD COLSPAN=<%$opt{colspan}%>>
@@ -360,6 +362,37 @@ sub pkg_status_row_changed {
   $html;
 }
 
+sub pkg_status_row_detached {
+  my( $cust_pkg, %opt ) = @_;
+
+  return '' unless $cust_pkg->change_custnum;
+
+  my $html = '';
+
+  my $cust_main = $cust_pkg->change_cust_main;
+  if ( $cust_main ) {
+
+    my $cust_link = '<A HREF="cust_main.cgi?'.  $cust_pkg->change_custnum. '">'.
+                      encode_entities( $cust_main->name ).
+                    '</A>';
+
+    my $what = $opt{'pkg_attached'} ? 'Attached' : 'Detached';
+
+    $html .= pkg_status_row_colspan( $cust_pkg, 
+                                     emt("$what to customer #[_1]: ",
+                                            $cust_pkg->change_custnum
+                                        ).
+                                       $cust_link,
+                                     '',
+                                     'size'    => '-1',
+                                     'align'   => 'right',
+                                     'colspan' => 4,
+                                   );
+  }
+
+  $html;
+}
+
 sub pkg_status_row_noauto {
   my( $cust_pkg, %opt ) = @_;
   my $part_pkg = $opt{'part_pkg'};
index 66008ee..915be49 100644 (file)
 
 %#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;
 %
 %  $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>
@@ -386,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;
 
@@ -533,12 +511,40 @@ foreach my $cust_refund ($cust_main->cust_refund) {
 
 }
 
-# sort history
+# 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 {
-  @history = sort { $a->{date} <=> $b->{date} } @history;
-} # no other sort orders for now
+} # else it's already oldest-first, and there are no other options yet
 
 sub translate_payby {
     my ($payby,$payinfo) = (shift,shift);
index d735195..997ac14 100644 (file)
@@ -51,8 +51,10 @@ function areyousure(href) {
 % } 
 
 <% mt('Service #') |h %><B><% $svcnum %></B>
-% my $url = $opt{'edit_url'} || $p. 'edit/'. $opt{'table'}. '.cgi?';
+% if ( $custnum ) {
+%   my $url = $opt{'edit_url'} || $p. 'edit/'. $opt{'table'}. '.cgi?';
 <& /view/elements/svc_edit_link.html, 'svc' => $svc_x, 'edit_url' => $url &>
+% }
 <BR>
 
 <% ntable("#cccccc") %><TR><TD><% ntable("#cccccc",2) %>
@@ -127,7 +129,9 @@ function areyousure(href) {
 
 % }
 
+% if ( $cust_svc ) {
 <& /elements/table-tickets.html, object => $cust_svc &>
+% }
 
 <% joblisting({'svcnum'=>$svcnum}, 1) %>
 
@@ -150,7 +154,7 @@ my $fields = $opt{'fields'}
 
 my $svcnum;
 if ( $cgi->param('svcnum') ) {
-  $cgi->param('svcnum') =~ /^(\d+)$/ or die "unparsable svcnum";
+  $cgi->param('svcnum') =~ /^(\d+)$/ or die "unparseable svcnum";
   $svcnum = $1;
 } else {
   my($query) = $cgi->keywords;
@@ -170,19 +174,29 @@ my $svc_x = qsearchs({
 }) or die "Unknown svcnum $svcnum in ". $opt{'table'}. " table\n";
 
 my $cust_svc = $svc_x->cust_svc;
-my($label, $value, $svcdb) = $cust_svc->label;
+my ($label, $value, $svcdb, $part_svc );
+my $labels = $opt{labels}; #not -> here
 
-my $part_svc = $cust_svc->part_svc;
+if ( $cust_svc ) {
+  ($label, $value, $svcdb) = $cust_svc->label;
 
-#false laziness w/edit/svc_Common.html
-#override default labels with service-definition labels if applicable
-my $labels = $opt{labels}; #not -> here
-foreach my $field ( keys %$labels ) {
-  my $col = $part_svc->part_svc_column($field);
-  $labels->{$field} = $col->columnlabel if $col->columnlabel !~ /^\s*$/;
+  $part_svc = $cust_svc->part_svc;
+
+  #false laziness w/edit/svc_Common.html
+  #override default labels with service-definition labels if applicable
+  foreach my $field ( keys %$labels ) {
+    my $col = $part_svc->part_svc_column($field);
+    $labels->{$field} = $col->columnlabel if $col->columnlabel !~ /^\s*$/;
+  }
+} else {
+  $label = "Unlinked $table";
+  $value = $svc_x->label;
+  $svcdb = $table;
+  # just to satisfy callbacks
+  $part_svc = FS::part_svc->new({ svcpart => 0, svcdb => $table });
 }
 
-my $pkgnum = $cust_svc->pkgnum;
+my $pkgnum = $cust_svc->pkgnum if $cust_svc;
 
 my($cust_pkg, $custnum);
 if ($pkgnum) {
index 7b46dc9..7e300b0 100644 (file)
@@ -7,7 +7,7 @@
 
 # false laziness w/edit/svc_Common.html
 
-$cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unparsable svcdb";
+$cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unparseable svcdb";
 my $table = $1;
 require "FS/$table.pm";
 
index 76631ba..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,
@@ -43,6 +45,9 @@
               %gopt,
 &>
 
+</FORM>
+
+
 <& svc_acct/basics.html,
               'svc_acct' => $svc_acct,
               'part_svc' => $part_svc,
index 05b6ac5..7d6520e 100644 (file)
@@ -36,6 +36,14 @@ my @fields = (
   #'longitude',
   { 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',
@@ -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 ) {