Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorIvan Kohler <ivan@freeside.biz>
Wed, 20 Jul 2016 20:02:15 +0000 (13:02 -0700)
committerIvan Kohler <ivan@freeside.biz>
Wed, 20 Jul 2016 20:02:15 +0000 (13:02 -0700)
52 files changed:
FS/FS/Conf.pm
FS/FS/Mason.pm
FS/FS/Schema.pm
FS/FS/Template_Mixin.pm
FS/FS/TicketSystem/RT_Internal.pm
FS/FS/Upgrade.pm
FS/FS/cust_location.pm
FS/FS/cust_main.pm
FS/FS/cust_pay.pm
FS/FS/cust_pay_void.pm
FS/FS/cust_payby.pm
FS/FS/cust_pkg.pm
FS/FS/cust_pkg_reason.pm
FS/FS/cust_refund.pm
FS/FS/cust_svc.pm
FS/FS/h_cust_svc.pm
FS/FS/msg_template.pm
FS/FS/part_pkg.pm
FS/FS/part_pkg/rt_field.pm [new file with mode: 0644]
FS/FS/part_svc.pm
FS/FS/part_svc_msgcat.pm [new file with mode: 0644]
FS/FS/payinfo_Mixin.pm
FS/FS/quotation.pm
FS/FS/rt_field_charge.pm [new file with mode: 0644]
conf/invoice_html
conf/invoice_latex
httemplate/browse/part_svc.cgi
httemplate/edit/elements/part_svc_column.html
httemplate/edit/part_pkg.cgi
httemplate/edit/part_svc.cgi
httemplate/edit/process/elements/process.html
httemplate/edit/process/part_pkg.cgi
httemplate/edit/process/quotation_convert.html
httemplate/edit/svc_acct.cgi
httemplate/edit/svc_acct/communigate.html
httemplate/elements/freeside.css
httemplate/elements/header-full.html
httemplate/elements/header-popup.html
httemplate/elements/printtofit.js [new file with mode: 0644]
httemplate/elements/progress-init.html
httemplate/elements/select-rt-customfield.html
httemplate/elements/select-rt-queue.html [new file with mode: 0644]
httemplate/elements/select-terms.html
httemplate/elements/tr-input-locale-text.html [new file with mode: 0644]
httemplate/elements/tr-select-router_block_ip.html
httemplate/elements/xmlhttp.html
httemplate/pref/pref-process.html
httemplate/pref/pref.html
httemplate/search/elements/cust_pay_or_refund.html
httemplate/view/quotation.html
httemplate/view/svc_acct/basics.html
rt/lib/RT/Search/UnrepliedTickets.pm

index a0eaab5..14bc1dc 100644 (file)
@@ -1,7 +1,8 @@
 package FS::Conf;
 
 use strict;
-use vars qw( $base_dir @config_items @base_items @card_types $DEBUG
+use vars qw( $base_dir @config_items @base_items @card_types @invoice_terms
+             $DEBUG
              $conf_cache $conf_cache_enabled
            );
 use Carp;
@@ -616,6 +617,14 @@ logo.png
 logo.eps
 );
 
+@invoice_terms = (
+  '',
+  'Payable upon receipt',
+  'Net 0', 'Net 3', 'Net 5', 'Net 7', 'Net 9', 'Net 10', 'Net 14', 
+  'Net 15', 'Net 18', 'Net 20', 'Net 21', 'Net 25', 'Net 30', 'Net 45', 
+  'Net 60', 'Net 90'
+);
+
 my %msg_template_options = (
   'type'        => 'select-sub',
   'options_sub' => sub { 
@@ -1521,11 +1530,8 @@ and customer address. Include units.',
     'description' => 'Optional default invoice term, used to calculate a due date printed on invoices.',
     'type'        => 'select',
     'per_agent'   => 1,
-    'select_enum' => [ 
-      '', 'Payable upon receipt', 'Net 0', 'Net 3', 'Net 5', 'Net 7', 'Net 9', 'Net 10', 'Net 14', 
-      'Net 15', 'Net 18', 'Net 20', 'Net 21', 'Net 25', 'Net 30', 'Net 45', 
-      'Net 60', 'Net 90'
-    ], },
+    'select_enum' => \@invoice_terms,
+  },
 
   { 
     'key'         => 'invoice_show_prior_due_date',
@@ -3441,13 +3447,6 @@ and customer address. Include units.',
   },
 
   {
-    'key'         => 'cust_pkg-always_show_location',
-    'section'     => 'packages',
-    'description' => "Always display package locations, even when they're all the default service address.",
-    'type'        => 'checkbox',
-  },
-
-  {
     'key'         => 'cust_pkg-group_by_location',
     'section'     => 'packages',
     'description' => "Group packages by location.",
@@ -3596,6 +3595,13 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'invoice-all_pkg_addresses',
+    'section'     => 'invoicing',
+    'description' => 'Show all package addresses on invoices, even the default.',
+    'type'        => 'checkbox',
+  },
+
+  {
     'key'         => 'invoice-unitprice',
     'section'     => 'invoicing',
     'description' => 'Enable unit pricing on invoices and quantities on packages.',
index 847f18b..0257b04 100644 (file)
@@ -412,6 +412,7 @@ if ( -e $addl_handler_use_file ) {
   use FS::fiber_olt;
   use FS::olt_site;
   use FS::access_user_page_pref;
+  use FS::part_svc_msgcat;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index a50b551..ac58510 100644 (file)
@@ -1693,7 +1693,7 @@ sub tables_hashref {
         'weight',          'int', 'NULL',        '', '', '', 
         'payby',          'char',     '',         4, '', '', 
         'payinfo',     'varchar', 'NULL',       512, '', '', 
-        'cardtype',    'varchar', 'NULL',   $char_d, '', '',
+        'paycardtype', 'varchar', 'NULL',   $char_d, '', '',
         'paycvv',      'varchar', 'NULL',       512, '', '', 
         'paymask',     'varchar', 'NULL',   $char_d, '', '', 
         #'paydate',   @date_type, '', '', 
@@ -2443,6 +2443,7 @@ sub tables_hashref {
         'usernum',         'int', 'NULL',      '', '', '',
         'payby',          'char',     '',       4, '', '',
         'payinfo',     'varchar', 'NULL',     512, '', '',
+        'paycardtype', 'varchar', 'NULL',   $char_d, '', '',
         'paymask',     'varchar', 'NULL', $char_d, '', '', 
         'paydate',     'varchar', 'NULL',      10, '', '', 
         'paybatch',    'varchar', 'NULL', $char_d, '', '',#for auditing purposes
@@ -2500,7 +2501,8 @@ sub tables_hashref {
         'usernum',         'int', 'NULL',      '', '', '',
         'payby',          'char',     '',       4, '', '',
         'payinfo',     'varchar', 'NULL',     512, '', '',
-       'paymask',     'varchar', 'NULL', $char_d, '', '', 
+        'paycardtype', 'varchar', 'NULL',   $char_d, '', '',
+        'paymask',     'varchar', 'NULL', $char_d, '', '', 
         #'paydate' ?
         'paybatch',    'varchar', 'NULL', $char_d, '', '', #for auditing purposes.
         'closed',        'char',  'NULL',       1, '', '', 
@@ -3059,7 +3061,8 @@ sub tables_hashref {
                                                      # be index into payby
                                                      # table eventually
         'payinfo',      'varchar',   'NULL', 512, '', '', #see cust_main above
-       'paymask', 'varchar', 'NULL', $char_d, '', '', 
+        'paycardtype',  'varchar', 'NULL',   $char_d, '', '',
+        'paymask', 'varchar', 'NULL', $char_d, '', '', 
         'paybatch',     'varchar',   'NULL', $char_d, '', '', 
         'closed',    'char', 'NULL', 1, '', '', 
         'source_paynum', 'int', 'NULL', '', '', '', # link to cust_payby, to prevent unapply of gateway-generated refunds
@@ -3682,6 +3685,24 @@ sub tables_hashref {
                         ],
     },
 
+    'part_svc_msgcat' => {
+      'columns' => [
+        'svcpartmsgnum',  'serial',     '',        '', '', '',
+        'svcpart',           'int',     '',        '', '', '',
+        'locale',        'varchar',     '',        16, '', '',
+        'svc',           'varchar',     '',   $char_d, '', '',
+      ],
+      'primary_key'  => 'svcpartmsgnum',
+      'unique'       => [ [ 'svcpart', 'locale' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'svcpart' ],
+                            table      => 'part_svc',
+                          },
+                        ],
+    },
+
+
     #(this should be renamed to part_pop)
     'svc_acct_pop' => {
       'columns' => [
@@ -7397,6 +7418,26 @@ sub tables_hashref {
                         ],
     },
 
+    'rt_field_charge' => {
+      'columns' => [
+        'rtfieldchargenum',    'serial',      '',      '', '', '',
+        'pkgnum',                 'int',      '',      '', '', '', 
+        'ticketid',               'int',      '',      '', '', '', 
+        'rate',             @money_type,                   '', '', 
+        'units',              'decimal',      '',  '10,4', '', '',
+        'charge',           @money_type,                   '', '', 
+        '_date',             @date_type,                   '', '',
+      ],
+      'primary_key'  => 'rtfieldchargenum',
+      'unique'       => [],
+      'index'        => [ ['pkgnum', 'ticketid'] ],
+      'foreign_keys' => [
+                          { columns    => [ 'pkgnum' ],
+                            table      => 'cust_pkg',
+                          },
+                        ],
+    },
+
     # name type nullability length default local
 
     #'new_table' => {
index 5153f87..c8ddffd 100644 (file)
@@ -3186,7 +3186,9 @@ sub _items_cust_bill_pkg {
 
   # for location labels: use default location on the invoice date
   my $default_locationnum;
-  if ( $self->custnum ) {
+  if ( $conf->exists('invoice-all_pkg_addresses') ) {
+    $default_locationnum = 0; # treat them all as non-default
+  } elsif ( $self->custnum ) {
     my $h_cust_main;
     my @h_search = FS::h_cust_main->sql_h_search($self->_date);
     $h_cust_main = qsearchs({
@@ -3320,6 +3322,7 @@ sub _items_cust_bill_pkg {
 
           # append the word 'Setup' to the setup line if there's going to be
           # a recur line for the same package (i.e. not a one-time charge) 
+          # XXX localization
           my $description = $desc;
           $description .= ' Setup'
             if $cust_bill_pkg->recur != 0
@@ -3340,8 +3343,11 @@ sub _items_cust_bill_pkg {
           # always pass the svc_label through to the template, even if 
           # not displaying it as an ext_description
           my @svc_labels = map &{$escape_function}($_),
-                      $cust_pkg->h_labels_short($self->_date, undef, 'I');
-
+            $cust_pkg->h_labels_short($self->_date,
+                                      undef,
+                                      'I',
+                                      $self->conf->{locale},
+                                     );
           $svc_label = $svc_labels[0];
 
           unless ( $cust_pkg->part_pkg->hide_svc_detail
@@ -3431,7 +3437,9 @@ sub _items_cust_bill_pkg {
           push @dates, undef if !$prev;
 
           my @svc_labels = map &{$escape_function}($_),
-                      $cust_pkg->h_labels_short(@dates, 'I');
+            $cust_pkg->h_labels_short(@dates,
+                                      'I',
+                                      $self->conf->{locale});
           $svc_label = $svc_labels[0];
 
           # show service labels, unless...
index ffee484..99e7044 100644 (file)
@@ -3,6 +3,7 @@ package FS::TicketSystem::RT_Internal;
 use strict;
 use vars qw( @ISA $DEBUG $me );
 use Data::Dumper;
+use Date::Format qw( time2str );
 use MIME::Entity;
 use FS::UID qw(dbh);
 use FS::CGI qw(popurl);
@@ -101,17 +102,43 @@ sub init {
   warn "$me init: complete" if $DEBUG;
 }
 
-=item customer_tickets CUSTNUM [ LIMIT ] [ PRIORITYVALUE ]
+=item customer_tickets CUSTNUM [ PARAMS ]
 
 Replacement for the one in RT_External so that we can access custom fields 
-properly.
+properly.  Accepts a hashref with the following parameters:
+
+number - custnum/svcnum
+
+limit 
+
+priority 
+
+status
+
+queueid
+
+resolved - only return tickets resolved after this timestamp
 
 =cut
 
 # create an RT::Tickets object for a specified custnum or svcnum
 
 sub _tickets_search {
-  my( $self, $type, $number, $limit, $priority, $status, $queueid ) = @_;
+  my $self = shift;
+  my $type = shift;
+
+  my( $number, $limit, $priority, $status, $queueid, $opt );
+  if ( ref($_[0]) eq 'HASH' ) {
+    $opt = shift;
+    $number   = $$opt{'number'};
+    $limit    = $$opt{'limit'};
+    $priority = $$opt{'priority'};
+    $status   = $$opt{'status'};
+    $queueid  = $$opt{'queueid'};
+  } else {
+    ( $number, $limit, $priority, $status, $queueid ) = @_;
+    $opt = {};
+  }
 
   $type =~ /^Customer|Service$/ or die "invalid type: $type";
   $number =~ /^\d+$/ or die "invalid custnum/svcnum: $number";
@@ -161,6 +188,10 @@ sub _tickets_search {
 
   $rtql .= " AND Queue = $queueid " if $queueid;
 
+  if ($$opt{'resolved'}) {
+    $rtql .= " AND Resolved >= " . dbh->quote(time2str('%Y-%m-%d %H:%M:%S',$$opt{'resolved'}));
+  }
+
   warn "$me _customer_tickets_search:\n$rtql\n" if $DEBUG;
   $Tickets->FromSQL($rtql);
 
@@ -255,7 +286,10 @@ sub _ticket_info {
   }
   $ticket_info{'owner'} = $t->OwnerObj->Name;
   $ticket_info{'queue'} = $t->QueueObj->Name;
+  $ticket_info{'_cf_sort_order'} = {};
+  my $cf_sort = 0;
   foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
+    $ticket_info{'_cf_sort_order'}{$CF->Name} = $cf_sort++;
     my $name = 'CF.{'.$CF->Name.'}';
     $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
   }
@@ -649,5 +683,49 @@ sub selfservice_priority {
   }
 }
 
+=item custom_fields
+
+Returns a hash of custom field names and descriptions.
+
+Accepts the following options:
+
+lookuptype - limit results to this lookuptype
+
+valuetype - limit results to this valuetype
+
+Fields must be visible to CurrentUser.
+
+=cut
+
+sub custom_fields {
+  my $self = shift;
+  my %opt = @_;
+  my $lookuptype = $opt{lookuptype};
+  my $valuetype = $opt{valuetype};
+
+  my $CurrentUser = RT::CurrentUser->new();
+  $CurrentUser->LoadByName($FS::CurrentUser::CurrentUser->username);
+  die "RT not configured" unless $CurrentUser->id;
+  my $CFs = RT::CustomFields->new($CurrentUser);
+
+  $CFs->UnLimit;
+
+  $CFs->Limit(FIELD => 'LookupType',
+              OPERATOR => 'ENDSWITH',
+              VALUE => $lookuptype)
+      if $lookuptype;
+
+  $CFs->Limit(FIELD => 'Type',
+              VALUE => $valuetype)
+      if $valuetype;
+
+  my @fields;
+  while (my $CF = $CFs->Next) {
+    push @fields, $CF->Name, ($CF->Description || $CF->Name);
+  }
+
+  return @fields;
+}
+
 1;
 
index a374d39..6f14cd2 100644 (file)
@@ -180,6 +180,14 @@ If you need to continue using the old Form 477 report, turn on the
 
   enable_banned_pay_pad() unless length($conf->config('banned_pay-pad'));
 
+  # if translate-auto-insert is enabled for a locale, ensure that invoice
+  # terms are in the msgcat (is there a better place for this?)
+  if (my $auto_locale = $conf->config('translate-auto-insert')) {
+    my $lh = FS::L10N->get_handle($auto_locale);
+    foreach (@FS::Conf::invoice_terms) {
+      $lh->maketext($_) if length($_);
+    }
+  }
 }
 
 sub upgrade_overlimit_groups {
@@ -414,6 +422,9 @@ sub upgrade_data {
     'cust_refund' => [],
     'banned_pay' => [],
 
+    #paycardtype
+    'cust_payby' => [],
+
     #default namespace
     'payment_gateway' => [],
 
index 0dec065..9040098 100644 (file)
@@ -722,9 +722,12 @@ sub label_prefix {
   } elsif ( $label_prefix eq '_location' && $self->locationname ) {
     $prefix = $self->locationname;
 
-  } elsif (    ( $opt{'cust_main'} || $self->custnum )
-          && $self->locationnum == $cust_or_prospect->ship_locationnum ) {
-    $prefix = 'Default service location';
+  #} elsif (    ( $opt{'cust_main'} || $self->custnum )
+  #        && $self->locationnum == $cust_or_prospect->ship_locationnum ) {
+  #  $prefix = 'Default service location';
+  #}
+  } else {
+    $prefix = '';
   }
 
   $prefix;
index 3fb0a87..2af6a1f 100644 (file)
@@ -29,6 +29,7 @@ use Date::Format;
 use File::Temp; #qw( tempfile );
 use Business::CreditCard 0.28;
 use List::Util qw(min);
+use Try::Tiny;
 use FS::UID qw( dbh driver_name );
 use FS::Record qw( qsearchs qsearch dbdef regexp_sql );
 use FS::Cursor;
@@ -76,6 +77,7 @@ use FS::upgrade_journal;
 use FS::sales;
 use FS::cust_payby;
 use FS::contact;
+use FS::reason;
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -2159,7 +2161,11 @@ FS::cust_pkg::cancel() methods.
 
 =item quiet - can be set true to supress email cancellation notices.
 
-=item reason - can be set to a cancellation reason (see L<FS:reason>), either a reasonnum of an existing reason, or passing a hashref will create a new reason.  The hashref should have the following keys: typenum - Reason type (see L<FS::reason_type>, reason - Text of the new reason.
+=item reason - can be set to a cancellation reason (see L<FS:reason>), either a
+reasonnum of an existing reason, or passing a hashref will create a new reason.
+The hashref should have the following keys:
+typenum - Reason type (see L<FS::reason_type>)
+reason - Text of the new reason.
 
 =item cust_pkg_reason - can be an arrayref of L<FS::cust_pkg_reason> objects
 for the individual packages, parallel to the C<cust_pkg> argument. The
@@ -2222,10 +2228,9 @@ sub cancel_pkgs {
   }
   dbh->commit;
 
-  $FS::UID::AutoCommit = 1;
   my @errors;
-  # now cancel all services, the same way we would for individual packages.
-  # if any of them fail, cancel the rest anyway.
+  # try to cancel each service, the same way we would for individual packages,
+  # but in cancel weight order.
   my @cust_svc = map { $_->cust_svc } @pkgs;
   my @sorted_cust_svc =
     map  { $_->[0] }
@@ -2238,8 +2243,15 @@ sub cancel_pkgs {
   foreach my $cust_svc (@sorted_cust_svc) {
     my $part_svc = $cust_svc->part_svc;
     next if ( defined($part_svc) and $part_svc->preserve );
-    my $error = $cust_svc->cancel; # immediate cancel, no date option
-    push @errors, $error if $error;
+    # immediate cancel, no date option
+    # transactionize individually
+    my $error = try { $cust_svc->cancel } catch { $_ };
+    if ( $error ) {
+      dbh->rollback;
+      push @errors, $error;
+    } else {
+      dbh->commit;
+    }
   }
   if (@errors) {
     return @errors;
@@ -2253,15 +2265,34 @@ sub cancel_pkgs {
   if ($opt{'cust_pkg_reason'}) {
     @cprs = @{ delete $opt{'cust_pkg_reason'} };
   }
+  my $null_reason;
   foreach (@pkgs) {
     my %lopt = %opt;
     if (@cprs) {
       my $cpr = shift @cprs;
-      $lopt{'reason'}        = $cpr->reasonnum;
-      $lopt{'reason_otaker'} = $cpr->otaker;
+      if ( $cpr ) {
+        $lopt{'reason'}        = $cpr->reasonnum;
+        $lopt{'reason_otaker'} = $cpr->otaker;
+      } else {
+        warn "no reason found when canceling package ".$_->pkgnum."\n";
+        # we're not actually required to pass a reason to cust_pkg::cancel,
+        # but if we're getting to this point, something has gone awry.
+        $null_reason ||= FS::reason->new_or_existing(
+          reason  => 'unknown reason',
+          type    => 'Cancel Reason',
+          class   => 'C',
+        );
+        $lopt{'reason'} = $null_reason->reasonnum;
+        $lopt{'reason_otaker'} = $FS::CurrentUser::CurrentUser->username;
+      }
     }
     my $error = $_->cancel(%lopt);
-    push @errors, 'pkgnum '.$_->pkgnum.': '.$error if $error;
+    if ( $error ) {
+      dbh->rollback;
+      push @errors, 'pkgnum '.$_->pkgnum.': '.$error;
+    } else {
+      dbh->commit;
+    }
   }
 
   return @errors;
index 331a156..e0a7143 100644 (file)
@@ -97,6 +97,10 @@ Payment Type (See L<FS::payinfo_Mixin> for valid values)
 
 Payment Information (See L<FS::payinfo_Mixin> for data format)
 
+=item paycardtype
+
+Credit card type, if appropriate; autodetected.
+
 =item paymask
 
 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
@@ -1205,6 +1209,12 @@ sub _upgrade_data {  #class method
       process_upgrade_paybatch();
     }
   }
+
+  ###
+  # set paycardtype
+  ###
+  $class->upgrade_set_cardtype;
+
 }
 
 sub process_upgrade_paybatch {
index 8d37a58..29540d1 100644 (file)
@@ -74,6 +74,10 @@ Payment Type (See L<FS::payinfo_Mixin> for valid values)
 
 card number, check #, or comp issuer (4-8 lowercase alphanumerics; think username), respectively
 
+=item cardtype
+
+Credit card type, if appropriate.
+
 =item paybatch
 
 text field for tracking card processing
index 62fa9be..e4a1d19 100644 (file)
@@ -115,6 +115,9 @@ paytype
 
 payip
 
+=item paycardtype
+
+The credit card type (deduced from the card number).
 
 =back
 
@@ -331,6 +334,13 @@ sub check {
   # Need some kind of global flag to accept invalid cards, for testing
   # on scrubbed data.
   #XXX if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
+
+  # In this block: detect card type; reject credit card / account numbers that
+  # are impossible or banned; reject other payment features (date, CVV length)
+  # that are inappropriate for the card type.
+  # However, if the payinfo is encrypted then just detect card type and assume
+  # the other checks were already done.
+
   if ( !$ignore_invalid_card && 
     $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
 
@@ -343,9 +353,12 @@ sub check {
     validate($payinfo)
       or return gettext('invalid_card'); # . ": ". $self->payinfo;
 
-    return gettext('unknown_card_type')
-      if $self->payinfo !~ /^99\d{14}$/ #token
-      && cardtype($self->payinfo) eq "Unknown";
+    my $cardtype = cardtype($payinfo);
+    $cardtype = 'Tokenized' if $self->payinfo =~ /^99\d{14}$/; #token
+    
+    return gettext('unknown_card_type') if $cardtype eq "Unknown";
+    
+    $self->set('paycardtype', $cardtype);
 
     unless ( $ignore_banned_card ) {
       my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
@@ -367,7 +380,7 @@ sub check {
     }
 
     if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
-      if ( cardtype($self->payinfo) eq 'American Express card' ) {
+      if ( $cardtype eq 'American Express card' ) {
         $self->paycvv =~ /^(\d{4})$/
           or return "CVV2 (CID) for American Express cards is four digits.";
         $self->paycvv($1);
@@ -380,7 +393,6 @@ sub check {
       $self->paycvv('');
     }
 
-    my $cardtype = cardtype($payinfo);
     if ( $cardtype =~ /^(Switch|Solo)$/i ) {
 
       return "Start date or issue number is required for $cardtype cards"
@@ -438,6 +450,15 @@ sub check {
       }
     }
 
+  } elsif ( $self->payby =~ /^CARD|DCRD$/ and $self->paymask ) {
+    # either ignoring invalid cards, or we can't decrypt the payinfo, but
+    # try to detect the card type anyway. this never returns failure, so
+    # the contract of $ignore_invalid_cards is maintained.
+    $self->set('paycardtype', cardtype($self->paymask));
+  } else {
+    $self->set('paycardtype', '');
+  }
+
 #  } elsif ( $self->payby eq 'PREPAY' ) {
 #
 #    my $payinfo = $self->payinfo;
@@ -449,8 +470,6 @@ sub check {
 #      unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
 #    $self->paycvv('');
 
-  }
-
   if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
 
     $self->paydate('');
@@ -458,6 +477,7 @@ sub check {
   } elsif ( $self->payby =~ /^(CARD|DCRD)$/ ) {
 
     # shouldn't payinfo_check do this?
+    # (except we don't ever call payinfo_check from here)
     return "Expiration date required"
       if $self->paydate eq '' || $self->paydate eq '-';
 
@@ -520,10 +540,14 @@ sub check_payinfo_cardtype {
   my $payinfo = $self->payinfo;
   $payinfo =~ s/\D//g;
 
-  return '' if $payinfo =~ /^99\d{14}$/; #token
+  if ( $payinfo =~ /^99\d{14}$/ ) {
+    $self->set('paycardtype', 'Tokenized');
+    return '';
+  }
 
   my %bop_card_types = map { $_=>1 } values %{ card_types() };
   my $cardtype = cardtype($payinfo);
+  $self->set('paycardtype', $cardtype);
 
   return "$cardtype not accepted" unless $bop_card_types{$cardtype};
 
@@ -599,7 +623,7 @@ sub label {
   my $self = shift;
 
   my $name = $self->payby =~ /^(CARD|DCRD)$/
-              && cardtype($self->paymask) || FS::payby->shortname($self->payby);
+              && $self->paycardtype || FS::payby->shortname($self->payby);
 
   ( $self->payby =~ /^(CARD|CHEK)$/  ? $weight{$self->weight}. ' automatic '
                                      : 'Manual '
@@ -872,6 +896,18 @@ sub search_sql {
 
 =back
 
+=cut
+
+sub _upgrade_data {
+
+  my $class = shift;
+  local $ignore_banned_card = 1;
+  local $ignore_expired_card = 1;
+  local $ignore_invalid_card = 1;
+  $class->upgrade_set_cardtype;
+
+}
+
 =head1 BUGS
 
 =head1 SEE ALSO
index 6616257..bbb281a 100644 (file)
@@ -533,6 +533,7 @@ sub delete {
   # cust_bill_pay.pkgnum (wtf, shouldn't reference pkgnum)
   # cust_pkg_usage.pkgnum
   # cust_pkg.uncancel_pkgnum, change_pkgnum, main_pkgnum, and change_to_pkgnum
+  # rt_field_charge.pkgnum
 
   # cust_svc is handled by canceling the package before deleting it
   # cust_pkg_option is handled via option_Common
@@ -2529,6 +2530,21 @@ sub change {
       return "transferring package notes: $error";
     }
   }
+
+  # transfer scheduled expire/adjourn reasons
+  foreach my $action ('expire', 'adjourn') {
+    if ( $cust_pkg->get($action) ) {
+      my $reason = $self->last_cust_pkg_reason($action);
+      if ( $reason ) {
+        $reason->set('pkgnum', $cust_pkg->pkgnum);
+        $error = $reason->replace;
+        if ( $error ) {
+          $dbh->rollback if $oldAutoCommit;
+          return "transferring $action reason: $error";
+        }
+      }
+    }
+  }
   
   my @new_supp_pkgs;
 
@@ -2609,6 +2625,19 @@ sub change {
     return "canceling old package: $error";
   }
 
+  # transfer rt_field_charge, if we're not changing pkgpart
+  # after billing of old package, before billing of new package
+  if ( $same_pkgpart ) {
+    foreach my $rt_field_charge ($self->rt_field_charge) {
+      $rt_field_charge->set('pkgnum', $cust_pkg->pkgnum);
+      $error = $rt_field_charge->replace;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "transferring rt_field_charge: $error";
+      }
+    }
+  }
+
   if ( $conf->exists('cust_pkg-change_pkgpart-bill_now') ) {
     #$self->cust_main
     my $error = $cust_pkg->cust_main->bill( 
@@ -3953,23 +3982,27 @@ sub labels {
   map { [ $_->label ] } $self->cust_svc;
 }
 
-=item h_labels END_TIMESTAMP [ START_TIMESTAMP ] [ MODE ]
+=item h_labels END_TIMESTAMP [, START_TIMESTAMP [, MODE [, LOCALE ] ] ]
 
 Like the labels method, but returns historical information on services that
 were active as of END_TIMESTAMP and (optionally) not cancelled before
 START_TIMESTAMP.  If MODE is 'I' (for 'invoice'), services with the 
 I<pkg_svc.hidden> flag will be omitted.
 
-Returns a list of lists, calling the label method for all (historical) services
-(see L<FS::h_cust_svc>) of this billing item.
+If LOCALE is passed, service definition names will be localized.
+
+Returns a list of lists, calling the label method for all (historical)
+services (see L<FS::h_cust_svc>) of this billing item.
 
 =cut
 
 sub h_labels {
   my $self = shift;
-  warn "$me _h_labels called on $self\n"
+  my ($end, $start, $mode, $locale) = @_;
+  warn "$me h_labels\n"
     if $DEBUG;
-  map { [ $_->label(@_) ] } $self->h_cust_svc(@_);
+  map { [ $_->label($end, $start, $locale) ] }
+        $self->h_cust_svc($end, $start, $mode);
 }
 
 =item labels_short
@@ -3982,15 +4015,15 @@ individual services rather than individual items.
 =cut
 
 sub labels_short {
-  shift->_labels_short( 'labels', @_ );
+  shift->_labels_short( 'labels' ); # 'labels' takes no further arguments
 }
 
-=item h_labels_short END_TIMESTAMP [ START_TIMESTAMP ]
+=item h_labels_short END_TIMESTAMP [, START_TIMESTAMP [, MODE [, LOCALE ] ] ]
 
 Like h_labels, except returns a simple flat list, and shortens long
-(currently >5 or the cust_bill-max_same_services configuration value) lists of
-identical services to one line that lists the service label and the number of
-individual services rather than individual items.
+(currently >5 or the cust_bill-max_same_services configuration value) lists
+of identical services to one line that lists the service label and the
+number of individual services rather than individual items.
 
 =cut
 
@@ -3998,6 +4031,9 @@ sub h_labels_short {
   shift->_labels_short( 'h_labels', @_ );
 }
 
+# takes a method name ('labels' or 'h_labels') and all its arguments;
+# maybe should be "shorten($self->h_labels( ... ) )"
+
 sub _labels_short {
   my( $self, $method ) = ( shift, shift );
 
index d11d05e..29b4b0a 100644 (file)
@@ -209,6 +209,54 @@ sub _upgrade_data { # class method
     FS::upgrade_journal->set_done('cust_pkg_reason__missing_reason');
   }
 
+  # Fix misplaced expire/suspend reasons due to package change (RT#71623).
+  # These will look like:
+  # - there is an expire reason linked to pkg1
+  # - pkg1 has been canceled before the reason's date
+  # - pkg2 was changed from pkg1, has an expire date equal to the reason's
+  #   date, and has no expire reason (check this later)
+
+  my $error;
+  foreach my $action ('expire', 'adjourn') {
+    # Iterate this, because a package could be scheduled to expire, then
+    # changed several times, and we need to walk the reason forward to the
+    # last one.
+    while(1) {
+      my @reasons = qsearch(
+        {
+          select    => 'cust_pkg_reason.*',
+          table     => 'cust_pkg_reason',
+          addl_from => ' JOIN cust_pkg pkg1 USING (pkgnum)
+                         JOIN cust_pkg pkg2 ON (pkg1.pkgnum = pkg2.change_pkgnum)',
+          hashref   => { 'action' => uc(substr($action, 0, 1)) },
+          extra_sql => " AND pkg1.cancel IS NOT NULL
+                         AND cust_pkg_reason.date > pkg1.cancel
+                         AND pkg2.$action = cust_pkg_reason.date"
+        });
+      last if !@reasons;
+      warn "Checking ".scalar(@reasons)." possible misplaced $action reasons.\n";
+      foreach my $cust_pkg_reason (@reasons) {
+        my $new_pkg = qsearchs('cust_pkg', { change_pkgnum => $cust_pkg_reason->pkgnum });
+        my $new_reason = $new_pkg->last_cust_pkg_reason($action);
+        if ($new_reason and $new_reason->_date == $new_pkg->get($action)) {
+          # the expiration reason has been recreated on the new package, so
+          # just delete the old one
+          warn "Cleaning $action reason from canceled pkg#" .
+               $cust_pkg_reason->pkgnum . "\n";
+          $error = $cust_pkg_reason->delete;
+        } else {
+          # then the old reason needs to be transferred
+          warn "Moving $action reason from canceled pkg#" .
+               $cust_pkg_reason->pkgnum .
+               " to new pkg#" . $new_pkg->pkgnum ."\n";
+          $cust_pkg_reason->set('pkgnum' => $new_pkg->pkgnum);
+          $error = $cust_pkg_reason->replace;
+        }
+        die $error if $error;
+      }
+    }
+  }
+
   #still can't fill in an action?  don't abort the upgrade
   local($ignore_empty_action) = 1;
 
index ced9540..4d2baa5 100644 (file)
@@ -82,6 +82,10 @@ Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
 
 Payment Information (See L<FS::payinfo_Mixin> for data format)
 
+=item paycardtype
+
+Detected credit card type, if appropriate; autodetected.
+
 =item paymask
 
 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
@@ -472,6 +476,9 @@ sub _upgrade_data {  # class method
   my ($class, %opts) = @_;
   $class->_upgrade_reasonnum(%opts);
   $class->_upgrade_otaker(%opts);
+
+  local $ignore_empty_reasonnum = 1;
+  $class->upgrade_set_cardtype;
 }
 
 =back
index 3f73483..08183b4 100644 (file)
@@ -702,10 +702,10 @@ sub pkg_cancel_date {
   return $cust_pkg->getfield('cancel') || '';
 }
 
-=item label
+=item label [ LOCALE ]
 
 Returns a list consisting of:
-- The name of this service (from part_svc)
+- The name of this service (from part_svc), optionally localized
 - A meaningful identifier (username, domain, or mail alias)
 - The table name (i.e. svc_domain) for this service
 - svcnum
@@ -714,7 +714,7 @@ Usage example:
 
   my($label, $value, $svcdb) = $cust_svc->label;
 
-=item label_long
+=item label_long [ LOCALE ]
 
 Like the B<label> method, except the second item in the list ("meaningful
 identifier") may be longer - typically, a full name is included.
@@ -727,20 +727,25 @@ sub label_long { shift->_label('svc_label_long', @_); }
 sub _label {
   my $self = shift;
   my $method = shift;
+  my $locale = shift;
   my $svc_x = $self->svc_x
     or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
 
-  $self->$method($svc_x);
+  $self->$method($svc_x, undef, undef, $locale);
 }
 
+# svc_label(_long) takes three arguments: end date, start date, locale
+# and FS::svc_*::label methods must accept those also, if they even care
+
 sub svc_label      { shift->_svc_label('label',      @_); }
 sub svc_label_long { shift->_svc_label('label_long', @_); }
 
 sub _svc_label {
   my( $self, $method, $svc_x ) = ( shift, shift, shift );
+  my ($end, $start, $locale) = @_;
 
   (
-    $self->part_svc->svc,
+    $self->part_svc->svc_locale($locale),
     $svc_x->$method(@_),
     $self->part_svc->svcdb,
     $self->svcnum
index 7b565ad..89a4cd7 100644 (file)
@@ -39,14 +39,14 @@ sub date_deleted {
   $self->h_date('delete');
 }
 
-=item label END_TIMESTAMP [ START_TIMESTAMP ] 
+=item label END_TIMESTAMP [ START_TIMESTAMP ] [ LOCALE ]
 
-Returns a label for this historical service, if the service was created before
-END_TIMESTAMP and (optionally) not deleted before START_TIMESTAMP.  Otherwise,
-returns an empty list.
+Returns a label for this historical service, if the service was created
+before END_TIMESTAMP and (optionally) not deleted before START_TIMESTAMP.
+Otherwise, returns an empty list.
 
 If a service is found, returns a list consisting of:
-- The name of this historical service (from part_svc)
+- The name of this historical service (from part_svc), optionally localized
 - A meaningful identifier (username, domain, or mail alias)
 - The table name (i.e. svc_domain) for this historical service
 
@@ -55,13 +55,34 @@ If a service is found, returns a list consisting of:
 sub label      { shift->_label('svc_label',      @_); }
 sub label_long { shift->_label('svc_label_long', @_); }
 
+# Parameters to _label:
+#
+# 1: the cust_svc method we should call to produce the label. (svc_label
+# and svc_label_long are defined in FS::cust_svc, not here, and take a svc_x
+# object as first argument.)
+# 2, 3: date range to use to find the h_svc_x, which will be passed to
+# svc_label(_long) and eventually have ->label called on it.
+# 4: locale, passed to svc_label(_long) also.
+#
+# however, if label is called with a locale only, must DTRT (this is a
+# FS::cust_svc subclass)
+
 sub _label {
   my $self = shift;
   my $method = shift;
+  my ($end, $start, $locale);
+  if (defined($_[0])) {
+    if ( $_[0] =~ /^\d+$/ ) {
+      ($end, $start, $locale) = @_;
+    } else {
+      $locale = shift;
+      $end = $self->history_date;
+    }
+  }
 
   #carp "FS::h_cust_svc::_label called on $self" if $DEBUG;
   warn "FS::h_cust_svc::_label called on $self for $method" if $DEBUG;
-  my $svc_x = $self->h_svc_x(@_);
+  my $svc_x = $self->h_svc_x($end, $start);
   return () unless $svc_x;
   my $part_svc = $self->part_svc;
 
@@ -71,7 +92,7 @@ sub _label {
   }
 
   my @label;
-  eval { @label = $self->$method($svc_x, @_); };
+  eval { @label = $self->$method($svc_x, $end, $start, $locale); };
 
   if ($@) {
     carp 'while resolving history record for svcdb/svcnum ' . 
@@ -85,9 +106,9 @@ sub _label {
 
 =item h_svc_x END_TIMESTAMP [ START_TIMESTAMP ] 
 
-Returns the FS::h_svc_XXX object for this service as of END_TIMESTAMP (i.e. an
-FS::h_svc_acct object or FS::h_svc_domain object, etc.) and (optionally) not
-cancelled before START_TIMESTAMP.
+Returns the FS::h_svc_XXX object for this service as of END_TIMESTAMP (i.e.
+an FS::h_svc_acct object or FS::h_svc_domain object, etc.) and (optionally)
+not cancelled before START_TIMESTAMP.
 
 =cut
 
index 1dd48cc..b890717 100644 (file)
@@ -93,6 +93,7 @@ sub extension_table { ''; } # subclasses don't HAVE to have extensions
 
 sub _rebless {
   my $self = shift;
+  return '' unless $self->msgclass;
   my $class = 'FS::msg_template::' . $self->msgclass;
   eval "use $class;";
   bless($self, $class) unless $@;
index 709e137..92943f2 100644 (file)
@@ -773,8 +773,12 @@ sub check {
 =item check_options
 
 For a passed I<$options> hashref, validates any options that
-have 'validate' subroutines defined (I<$options> values might
-be altered.)  Returns error message, or empty string if valid.
+have 'validate' subroutines defined in the info hash, 
+then validates the entire hashref if the price plan has 
+its own 'validate' subroutine defined in the info hash 
+(I<$options> values might be altered.)  
+
+Returns error message, or empty string if valid.
 
 Invoked by L</insert> and L</replace> via the equivalent
 methods in L<FS::option_Common>.
@@ -793,6 +797,10 @@ sub check_options {
       }
     } # else "option does not exist" error?
   }
+  if (exists($plans{$self->plan}->{'validate'})) {
+    my $error = &{$plans{$self->plan}->{'validate'}}($options);
+    return $error if $error;
+  }
   return '';
 }
 
diff --git a/FS/FS/part_pkg/rt_field.pm b/FS/FS/part_pkg/rt_field.pm
new file mode 100644 (file)
index 0000000..657a8d7
--- /dev/null
@@ -0,0 +1,207 @@
+package FS::part_pkg::rt_field;
+
+use strict;
+use FS::Conf;
+use FS::TicketSystem;
+use FS::Record qw(qsearchs qsearch);
+use FS::part_pkg::recur_Common;
+use FS::part_pkg::global_Mixin;
+use FS::rt_field_charge;
+
+our @ISA = qw(FS::part_pkg::recur_Common);
+
+our $DEBUG = 0;
+
+use vars qw( $conf $money_char );
+
+FS::UID->install_callback( sub {
+  $conf = new FS::Conf;
+  $money_char = $conf->config('money_char') || '$';
+});
+
+my %custom_field = (
+  'type'        => 'select-rt-customfield',
+  'lookuptype'  => 'RT::Queue-RT::Ticket',
+);
+
+my %multiple = (
+  'multiple' => 1,
+  'parse' => sub { @_ }, # because /edit/process/part_pkg.pm doesn't grok select multiple
+);
+
+our %info = (
+  'name'      =>  'Bill from custom fields in resolved RT tickets',
+  'shortname' =>  'RT custom rate',
+  'weight'    => 65,
+  'inherit_fields' => [ 'global_Mixin' ],
+  'fields'    =>  {
+    'queueids'       => { 'name' => 'Queues',
+                          'type' => 'select-rt-queue',
+                          %multiple,
+                        },
+    'unit_field'     => { 'name' => 'Units field',
+                          %custom_field,
+                          'validate' => sub { return ${$_[1]} ? '' : 'Units field must be specified' },
+                        },
+    'rate_field'     => { 'name' => 'Charge per unit (from RT field)',
+                          %custom_field,
+                          'empty_label' => '',
+                        },
+       'rate_flat'      => { 'name' => 'Charge per unit (flat)',
+                          'validate' => \&FS::part_pkg::global_Mixin::validate_moneyn },
+    'display_fields' => { 'name' => 'Display fields',
+                          %custom_field,
+                          %multiple,
+                        },
+    # from global_Mixin, but don't get used by this at all
+    'unused_credit_cancel'  => {'disabled' => 1},
+    'unused_credit_suspend' => {'disabled' => 1},
+    'unused_credit_change'  => {'disabled' => 1},
+  },
+  'validate' => sub {
+    my $options = shift;
+    return 'Rate must be specified'
+      unless $options->{'rate_field'} or $options->{'rate_flat'};
+    return 'Cannot specify both flat rate and rate field'
+      if $options->{'rate_field'} and $options->{'rate_flat'};
+    return '';
+  },
+  'fieldorder' => [ 'queueids', 'unit_field', 'rate_field', 'rate_flat', 'display_fields' ]
+);
+
+sub price_info {
+    my $self = shift;
+    my $str = $self->SUPER::price_info;
+    $str .= ' plus ' if $str;
+    $str .= 'charge from RT';
+# takes way too long just to get a package label
+#    FS::TicketSystem->init();
+#    my %custom_fields = FS::TicketSystem->custom_fields();
+#    my $rate = $self->option('rate_flat',1);
+#    my $rate_field = $self->option('rate_field',1);
+#    my $unit_field = $self->option('unit_field');
+#    $str .= $rate
+#            ? $money_char . sprintf("%.2",$rate)
+#            : $custom_fields{$rate_field};
+#    $str .= ' x ' . $custom_fields{$unit_field};
+    return $str;
+}
+
+sub calc_setup {
+  my($self, $cust_pkg ) = @_;
+  $self->option('setup_fee');
+}
+
+sub calc_recur {
+  my $self = shift;
+  my($cust_pkg, $sdate, $details, $param ) = @_;
+
+  my $charges = 0;
+
+  $charges += $self->calc_usage(@_);
+  $charges += ($cust_pkg->quantity || 1) * $self->calc_recur_Common(@_);
+
+  $charges;
+
+}
+
+sub can_discount { 0; }
+
+sub calc_usage {
+  my $self = shift;
+  my($cust_pkg, $sdate, $details, $param ) = @_;
+
+  FS::TicketSystem->init();
+
+  my %queues = FS::TicketSystem->queues(undef,'SeeCustomField');
+
+  my @tickets;
+  foreach my $queueid (
+    split(', ',$self->option('queueids',1) || '')
+  ) {
+
+    die "Insufficient permission to invoice package"
+      unless exists $queues{$queueid};
+
+    # load all resolved tickets since pkg was ordered
+    # will subtract previous charges below
+    # only way to be sure we've caught everything
+    my $tickets = FS::TicketSystem->customer_tickets({
+      number   => $cust_pkg->custnum, 
+      limit    => 10000, # arbitrarily large
+      status   => 'resolved',
+      queueid  => $queueid,
+      resolved => $cust_pkg->order_date, # or setup? but this is mainly for installations,
+                                         # and workflow might resolve tickets before first bill...
+                                         # for now, expect pkg to be ordered before tickets get resolved,
+                                         # easy enough to make a pkg option to use setup/sdate instead
+    });
+    push @tickets, @$tickets;
+  };
+
+  my $rate = $self->option('rate_flat',1);
+  my $rate_field = $self->option('rate_field',1);
+  my $unit_field = $self->option('unit_field');
+  my @display_fields = split(', ',$self->option('display_fields',1) || '');
+
+  my %custom_fields = FS::TicketSystem->custom_fields();
+  my $rate_label = $rate
+                   ? ''
+                   : ' ' . $custom_fields{$rate_field};
+  my $unit_label = $custom_fields{$unit_field};
+
+  $rate_field = 'CF.{' . $rate_field . '}' if $rate_field;
+  $unit_field = 'CF.{' . $unit_field . '}';
+
+  my $charges = 0;
+  foreach my $ticket ( @tickets ) {
+    next unless $ticket->{$unit_field};
+    next unless $rate || $ticket->{$rate_field};
+    my $trate = $rate || $ticket->{$rate_field};
+    my $tunit = $ticket->{$unit_field};
+    my $subcharge = sprintf('%.2f', $trate * $tunit);
+    my $precharge = _previous_charges( $cust_pkg->pkgnum, $ticket->{'id'} );
+    $subcharge -= $precharge;
+
+    # if field values for previous charges increased,
+    # we can make additional charges here and now,
+    # but if field values were decreased, we just ignore--
+    # credits will have to be applied manually later, if that's what's intended
+    next if $subcharge <= 0;
+
+    my $rt_field_charge = new FS::rt_field_charge {
+      'pkgnum' => $cust_pkg->pkgnum,
+      'ticketid' => $ticket->{'id'},
+      'rate' => $trate,
+      'units' => $tunit,
+      'charge' => $subcharge,
+      '_date' => $$sdate,
+    };
+    my $error = $rt_field_charge->insert;
+    die "Error inserting rt_field_charge: $error" if $error;
+    push @$details, $money_char . sprintf('%.2f',$trate) . $rate_label . ' x ' . $tunit . ' ' . $unit_label;
+    push @$details, ' - ' . $money_char . sprintf('%.2f',$precharge) . ' previously charged' if $precharge;
+    foreach my $field (
+      sort { $ticket->{'_cf_sort_order'}{$a} <=> $ticket->{'_cf_sort_order'}{$b} } @display_fields
+    ) {
+      my $label = $custom_fields{$field};
+      my $value = $ticket->{'CF.{' . $field . '}'};
+      push @$details, $label . ': ' . $value if $value;
+    }
+    $charges += $subcharge;
+  }
+  return $charges;
+}
+
+sub _previous_charges {
+  my ($pkgnum, $ticketid) = @_;
+  my $prev = 0;
+  foreach my $rt_field_charge (
+    qsearch('rt_field_charge', { pkgnum => $pkgnum, ticketid => $ticketid })
+  ) {
+    $prev += $rt_field_charge->charge;
+  }
+  return $prev;
+}
+
+1;
index 621a554..dcc7843 100644 (file)
@@ -1,5 +1,5 @@
 package FS::part_svc;
-use base qw(FS::Record);
+use base qw(FS::o2m_Common FS::Record);
 
 use strict;
 use vars qw( $DEBUG );
@@ -11,6 +11,7 @@ use FS::part_export;
 use FS::export_svc;
 use FS::cust_svc;
 use FS::part_svc_class;
+use FS::part_svc_msgcat;
 
 FS::UID->install_callback(sub {
     # preload the cache and make sure all modules load
@@ -621,6 +622,24 @@ sub svc_x {
   map { $_->svc_x } $self->cust_svc;
 }
 
+=item svc_locale LOCALE
+
+Returns a customer-viewable service definition label in the chosen LOCALE.
+If there is no entry for that locale or if LOCALE is empty, returns
+part_svc.svc.
+
+=cut
+
+sub svc_locale {
+  my( $self, $locale ) = @_;
+  return $self->svc unless $locale;
+  my $part_svc_msgcat = qsearchs('part_svc_msgcat', {
+    svcpart => $self->svcpart,
+    locale  => $locale
+  }) or return $self->svc;
+  $part_svc_msgcat->svc;
+}
+
 =back
 
 =head1 CLASS METHODS
@@ -883,6 +902,12 @@ sub process {
     $param->{'svcpart'} = $new->getfield('svcpart');
   }
 
+  $error ||= $new->process_o2m(
+    'table'   => 'part_svc_msgcat',
+    'params'  => $param,
+    'fields'  => [ 'locale', 'svc' ],
+  );
+
   die "$error\n" if $error;
 }
 
diff --git a/FS/FS/part_svc_msgcat.pm b/FS/FS/part_svc_msgcat.pm
new file mode 100644 (file)
index 0000000..6d69198
--- /dev/null
@@ -0,0 +1,131 @@
+package FS::part_svc_msgcat;
+use base qw( FS::Record );
+
+use strict;
+use FS::Locales;
+
+=head1 NAME
+
+FS::part_svc_msgcat - Object methods for part_svc_msgcat records
+
+=head1 SYNOPSIS
+
+  use FS::part_svc_msgcat;
+
+  $record = new FS::part_svc_msgcat \%hash;
+  $record = new FS::part_svc_msgcat { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_svc_msgcat object represents localized labels of a service 
+definition.  FS::part_svc_msgcat inherits from FS::Record.  The following
+fields are currently supported:
+
+=over 4
+
+=item svcpartmsgnum
+
+primary key
+
+=item svcpart
+
+Service definition
+
+=item locale
+
+locale
+
+=item svc
+
+Localized service name (customer-viewable)
+
+=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_svc_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('svcpartmsgnum')
+    || $self->ut_foreign_key('svcpart', 'part_svc', 'svcpart')
+    || $self->ut_enum('locale', [ FS::Locales->locales ] )
+    || $self->ut_text('svc')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::part_svc>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index 4176818..4f26e8c 100644 (file)
@@ -5,6 +5,7 @@ use Business::CreditCard;
 use FS::payby;
 use FS::Record qw(qsearch);
 use FS::UID qw(driver_name);
+use FS::Cursor;
 use Time::Local qw(timelocal);
 
 use vars qw($ignore_masked_payinfo);
@@ -193,7 +194,12 @@ sub payinfo_check {
     or return "Illegal payby: ". $self->payby;
 
   if ( $self->payby eq 'CARD' && ! $self->is_encrypted($self->payinfo) ) {
+
     my $payinfo = $self->payinfo;
+    my $cardtype = cardtype($payinfo);
+    $cardtype = 'Tokenized' if $payinfo =~ /^99\d{14}$/;
+    $self->set('paycardtype', $cardtype);
+
     if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
       # allow it
     } else {
@@ -204,13 +210,18 @@ sub payinfo_check {
           or return "Illegal (mistyped?) credit card number (payinfo)";
         $self->payinfo($1);
         validate($self->payinfo) or return "Illegal credit card number";
-        return "Unknown card type" if $self->payinfo !~ /^99\d{14}$/ #token
-                                   && cardtype($self->payinfo) eq "Unknown";
+        return "Unknown card type" if $cardtype eq "Unknown";
       } else {
         $self->payinfo('N/A'); #???
       }
     }
   } else {
+    if ( $self->payby eq 'CARD' and $self->paymask ) {
+      # if we can't decrypt the card, at least detect the cardtype
+      $self->set('paycardtype', cardtype($self->paymask));
+    } else {
+      $self->set('paycardtype', '');
+    }
     if ( $self->is_encrypted($self->payinfo) ) {
       #something better?  all it would cause is a decryption error anyway?
       my $error = $self->ut_anything('payinfo');
@@ -404,6 +415,28 @@ sub paydate_epoch_sql {
   END"
 }
 
+=item upgrade_set_cardtype
+
+Find all records with a credit card payment type and no paycardtype, and
+replace them in order to set their paycardtype.
+
+=cut
+
+sub upgrade_set_cardtype {
+  my $class = shift;
+  # assign cardtypes to CARD/DCRDs that need them; check_payinfo_cardtype
+  # will do this. ignore any problems with the cards.
+  local $ignore_masked_payinfo = 1;
+  my $search = FS::Cursor->new({
+    table     => $class->table,
+    extra_sql => q[ WHERE payby IN('CARD','DCRD') AND paycardtype IS NULL ],
+  });
+  while (my $record = $search->fetch) {
+    my $error = $record->replace;
+    die $error if $error;
+  }
+}
+
 =back
 
 =head1 BUGS
index 0549853..c61e001 100644 (file)
@@ -350,7 +350,7 @@ sub _items_sections {
 
 sub enable_previous { 0 }
 
-=item convert_cust_main
+=item convert_cust_main [ PARAMS ]
 
 If this quotation already belongs to a customer, then returns that customer, as
 an FS::cust_main object.
@@ -362,10 +362,13 @@ packages as real packages for the customer.
 If there is an error, returns an error message, otherwise, returns the
 newly-created FS::cust_main object.
 
+Accepts the same params as L</order>.
+
 =cut
 
 sub convert_cust_main {
   my $self = shift;
+  my $params = shift || {};
 
   my $cust_main = $self->cust_main;
   return $cust_main if $cust_main; #already converted, don't again
@@ -382,7 +385,7 @@ sub convert_cust_main {
 
   $self->prospectnum('');
   $self->custnum( $cust_main->custnum );
-  my $error = $self->replace || $self->order;
+  my $error = $self->replace || $self->order(undef,$params);
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -394,7 +397,7 @@ sub convert_cust_main {
 
 }
 
-=item order [ HASHREF ]
+=item order [ HASHREF ] [ PARAMS ]
 
 This method is for use with quotations which are already associated with a customer.
 
@@ -406,11 +409,16 @@ If HASHREF is passed, it will be filled with a hash mapping the
 C<quotationpkgnum> of each quoted package to the C<pkgnum> of the package
 as ordered.
 
+If PARAMS hashref is passed, the following params are accepted:
+
+onhold - if true, suspends newly ordered packages
+
 =cut
 
 sub order {
   my $self = shift;
   my $pkgnum_map = shift || {};
+  my $params = shift || {};
   my $details_map = {};
 
   tie my %all_cust_pkg, 'Tie::RefHash';
@@ -461,10 +469,11 @@ sub order {
     }
   }
 
-  foreach my $quotationpkgnum (keys %$pkgnum_map) {
-    # convert the objects to just pkgnums
-    my $cust_pkg = $pkgnum_map->{$quotationpkgnum};
-    $pkgnum_map->{$quotationpkgnum} = $cust_pkg->pkgnum;
+  if ($$params{'onhold'}) {
+    foreach my $quotationpkgnum (keys %$pkgnum_map) {
+      last if $error;
+      $error = $pkgnum_map->{$quotationpkgnum}->suspend();
+    }
   }
 
   if ($error) {
@@ -473,6 +482,13 @@ sub order {
   }
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  foreach my $quotationpkgnum (keys %$pkgnum_map) {
+    # convert the objects to just pkgnums
+    my $cust_pkg = $pkgnum_map->{$quotationpkgnum};
+    $pkgnum_map->{$quotationpkgnum} = $cust_pkg->pkgnum;
+  }
+
   ''; #no error
 
 }
diff --git a/FS/FS/rt_field_charge.pm b/FS/FS/rt_field_charge.pm
new file mode 100644 (file)
index 0000000..fb01f81
--- /dev/null
@@ -0,0 +1,132 @@
+package FS::rt_field_charge;
+use base qw( FS::Record );
+
+use strict;
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::rt_field_charge - Object methods for rt_field_charge records
+
+=head1 SYNOPSIS
+
+  use FS::rt_field_charge;
+
+  $record = new FS::rt_field_charge \%hash;
+  $record = new FS::rt_field_charge { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::rt_field_charge object represents an individual charge
+that has been added to an invoice by a package with the rt_field price plan.
+FS::rt_field_charge inherits from FS::Record.
+The following fields are currently supported:
+
+=over 4
+
+=item rtfieldchargenum - primary key
+
+=item pkgnum - cust_pkg that generated the charge
+
+=item ticketid - RT ticket that generated the charge
+
+=item rate - the rate per unit for the charge
+
+=item units - quantity of units being charged
+
+=item charge - the total amount charged
+
+=item _date - billing date for the charge
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new object.  To add the object 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 { 'rt_field_charge'; }
+
+=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 object.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('rtfieldchargenum')
+    || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum' )
+    || $self->ut_number('ticketid')
+    || $self->ut_money('rate')
+    || $self->ut_float('units')
+    || $self->ut_money('charge')
+    || $self->ut_number('_date')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
index 9d6f528..d8a2d0a 100644 (file)
            $OUT .= qq! <img src="cust_bill-barcode.cgi?invnum=$invnum;template=$template"><br> !;
        }
     %>
-        <%= $terms ? emt('Terms') . ": $terms" : '' %><BR>
+        <%= $terms ? emt('Terms') . ': ' . emt($terms) : '' %><BR>
         <%= $po_line %>
       </td>
     </tr>
index 2a0f882..c1d04d6 100644 (file)
@@ -20,6 +20,7 @@
 \documentclass[letterpaper]{article}\r
 \r
 \usepackage{fancyhdr,lastpage,ifthen,array,longtable,afterpage,caption,multirow,bigstrut}\r
+\usepackage[breakwords]{truncate} % to avoid overflowing boxes\r
 \usepackage{graphicx}                  % required for logo graphic\r
 \usepackage[utf8]{inputenc}             % multilanguage support\r
 \usepackage[T1]{fontenc}\r
   \hline\r
   \rule{0pt}{2.5ex}\r
   \makebox[1.4cm]{} &\r
-  \multicolumn{\FSdescriptioncolumncount}{l}{\makebox[\FSdescriptionlength][l]{\textbf{[@-- emt('Description') --@]}}}&\r
+  \multicolumn{\FSdescriptioncolumncount}{l}{\r
+    \truncate{\FSdescriptionlength}{\textbf{[@-- emt('Description') --@]}}\r
+  } &\r
   \FSunitcolumns\r
   \makebox[1.6cm][r]{\textbf{[@-- emt('Amount') --@]}} \\\r
   \hline\r
   \rule{0pt}{2.5ex}\r
   \makebox[1.4cm]{} &\r
   \multicolumn{4}{l}{\r
-    \makebox[\FSdescriptionlength][l]{\textbf{[@-- emt('Description') --@]}}\r
+    \truncate{\FSdescriptionlength}{\textbf{[@-- emt('Description') --@]}}\r
   } &\r
   \textbf{~~[@-- emt('Calls') --@]} &\r
   \textbf{~~[@-- emt('Duration') --@]} &\r
 % ...description...\r
 \newcommand{\FSdesc}[5]{\r
   \multicolumn{1}{c}{\rule{0pt}{2.5ex}\textbf{#1}} &\r
-  \multicolumn{[@-- $unitprices ? '4' : '6' --@]}{l}{\textbf{#2}} &\r
+  \multicolumn{[@-- $unitprices ? '4' : '6' --@]}{l}{\r
+    \truncate{\FSdescriptionlength}{\textbf{#2}}\r
+  } &\r
 [@-- $unitprices ? '  \multicolumn{1}{r}{\textbf{#3}} &'."\n".\r
                    '  \multicolumn{1}{r}{\textbf{#4}} &'."\n"\r
                  : ''\r
 % ...extended description...\r
 \newcommand{\FSextdesc}[1]{\r
   \multicolumn{1}{l}{\rule{0pt}{1.0ex}} &\r
-%%  \multicolumn{2}{l}{\small{~-~#1}}\\\r
-#1\\\r
+  \multicolumn{6}{l}{\r
+    \truncate{12.8cm}{\small{~~~#1}}\r
+  } \\\r
 }\r
-% ...and total line items.\r
+% ...call detail (multiple columns already)...\r
+\newcommand{\FScalldetail}[1]{\r
+  \multicolumn{1}{l}{\rule{0pt}{1.0ex}} &\r
+  ~~~#1\r
+  \\\r
+}\r
+}\r
+% ...and total line items (which use the full 12.8cm length, ignoring\r
+% unitprice/quantity\r
 \newcommand{\FStotaldesc}[2]{\r
-  & \multicolumn{6}{l}{#1} & #2\\\r
+  & \multicolumn{6}{l}{\r
+    \truncate{12.8cm}{#1}\r
+  } & #2\\\r
 }\r
 \r
 % ...usage class summary\r
   }\r
 --@]\r
 \begin{flushright}\r
-[@-- $terms ? emt('Terms') .": $terms" : '' --@]\\\r
+[@-- $terms ? emt('Terms') . ': ' . emt($terms) : '' --@]\\\r
 [@-- $po_line --@]\\\r
 \end{flushright}\r
 \end{minipage}}\r
         foreach my $ext_desc (@$ext_description) {\r
           if ($section->{extended_description_generator}) {\r
             $OUT .= &{$section->{extended_description_generator}}($ext_desc);\r
-          } else {\r
-            if ( $ext_desc !~ /[^\\]&/ ) {\r
-              $ext_desc = substr($ext_desc, 0, 80) . '...'\r
-                if (length($ext_desc) > 80);\r
-              $ext_desc = '\multicolumn{6}{l}{\small{~~~'. $ext_desc. '}}';\r
-            }else{\r
-              $ext_desc = "~~~$ext_desc";\r
-            }\r
-            $OUT .= '\FSextdesc{' . $ext_desc . '}' . "${rowbreak}\n";\r
+          } elsif ( $ext_desc !~ /[^\\]&/ ) {\r
+            $OUT .= '\FSextdesc{' . $ext_desc . "}$rowbreak\n";\r
+          } else { # call detail\r
+            $OUT .= '\FScalldetail{' . $ext_desc . "}$rowbreak\n";\r
           }\r
         }\r
 \r
index dee4394..b947463 100755 (executable)
@@ -112,8 +112,24 @@ function part_export_areyousure(href) {
     </TD>
 % } 
 
-    <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF="<% $url %>">
-      <% $part_svc->svc %></A></TD>
+    <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <A HREF="<% $url %>">
+        <% $part_svc->svc %>
+      </A>
+%   # any alternate names of the service
+%   my %msgcat = map { $_->locale => $_ } $part_svc->part_svc_msgcat;
+%   my %labels = map { $_ => FS::Locales->description($_) } keys %msgcat;
+%   my @locales = sort { $labels{$a} cmp $labels{$b} } keys %msgcat;
+%   if ( @locales ) {
+      <BR>
+      <FONT SIZE="-1">
+%     foreach my $locale (@locales) {
+        <% $labels{$locale} %>: <% $msgcat{$locale}->get('svc') %>
+        <BR>
+%     }
+      </FONT>
+%   }
+    </TD>
 
     <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
       <% $svcdb %></TD>
index 4e112c0..816f342 100644 (file)
@@ -267,8 +267,9 @@ my %communigate_fields = (
 <& /elements/progress-init.html,
   $svcdb, #form name
   [ # form fields to send
-    qw(svc svcpart classnum selfservice_access disabled preserve exportnum),
-    @fields
+    'ALL'
+#    qw(svc svcpart classnum selfservice_access disabled preserve exportnum),
+#    @fields
   ],
   'process/part_svc.cgi',   # target
   $p.'browse/part_svc.cgi', # redirect landing
index 80a61f8..7fe659f 100755 (executable)
@@ -92,7 +92,7 @@
                    { type => 'columnstart' },
                    
                      { field     => 'pkg',
-                       type      => 'text',
+                       type      => 'input-locale-text',
                        size      => 40, #32
                        maxlength => 50,
                      },
@@ -495,42 +495,6 @@ 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 ) = @_;
 
@@ -579,16 +543,6 @@ 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'
-  );
-
   if ( $cgi->param('error') =~ / is suggested with / ) {
     #yeah, detection is a shitty kludge, but we don't have exception objects
     $opt->{form_init} = '<INPUT TYPE="checkbox" NAME="part_pkg_restrict_soft_override" VALUE="Y"> Override suggestion<BR><BR>';
@@ -665,20 +619,6 @@ 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 {
@@ -692,8 +632,6 @@ my $new_callback = sub {
 
   $options{'suspend_bill'}=1 if $conf->exists('part_pkg-default_suspend_bill');
 
-  &$splice_locale_fields($fields, '', '');
-
 };
 
 my $clone_callback = sub {
@@ -732,17 +670,6 @@ my $clone_callback = sub {
       foreach keys %part_pkg_currency;
   }
 
-  $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 {
@@ -1061,6 +988,16 @@ my $html_bottom = sub {
                    : ''
                  ). '>';
 
+      } elsif ( $href->{$field}{'type'} =~ /^select-rt-/ ) {
+
+        $html .= include('/elements/'.$href->{$field}{'type'}.'.html',
+                           'name'       => $layer.'__'.$field,
+                           'curr_value' => $options{$field},
+                           map { $_ => $href->{$field}{$_} }
+                             grep { $_ !~ /^(name|type|parse)$/ }
+                               keys %{ $href->{$field} }
+                        );
+
       } elsif ( $href->{$field}{'type'} eq 'select-rate' ) {
 
         $html .= include('/elements/select-rate.html',
index a07fc60..fed2125 100755 (executable)
 }
 </STYLE>
 <SCRIPT TYPE="text/javascript">
+// copy all fields from the outer form (svc and its localizations, plus
+// preserve, selfservice_access, etc.) into the inner form, creating hidden
+// inputs if needed
 function fixup_submit(layer) {
-  document.forms[layer].submit.disabled = true;
-  fixup(document.forms[layer]);
+  var layer_form = $(document.forms[layer]);
+  var main_form = $(document.forms['SvcEditMain']);
+  var data = main_form.serializeArray();
+  for (var i = 0; i < data.length; i++) {
+    var input = layer_form.children('[name=' + data[i].name + ']');
+    if (input[0]) {
+      input.prop('value', data[i].value);
+    } else {
+      $( '<input type="hidden">' )
+        .attr('name', data[i].name)
+        .prop('value', data[i].value)
+        .appendTo(layer_form);
+    }
+  }
+  layer_form[0]['submit'].disabled = true;
+  //fixup(document.forms[layer]);
   window[layer+'process'].call();
 }
 
@@ -141,19 +158,26 @@ window.onload = function() {
 
 </SCRIPT>
 
-<FORM NAME="dummy">
+<FORM NAME="SvcEditMain">
 
 <FONT CLASS="fsinnerbox-title">Service Part #<% $part_svc->svcpart ? $part_svc->svcpart : "(NEW)" %></FONT>
 <TABLE CLASS="fsinnerbox">
-<TR>
-  <TD ALIGN="right">Service</TD>
-  <TD><INPUT TYPE="text" NAME="svc" VALUE="<% $hashref->{svc} %>"></TD>
-<TR>
+<& /elements/tr-input-locale-text.html,
+  'object' => $part_svc,
+  'cgi'    => $cgi,
+  'field'  => 'svc',
+  'label'  => 'Service',
+  'curr_value' => $hashref->{svc},
+&>
+%#<TR>
+%#  <TD ALIGN="right">Service</TD>
+%#  <TD><INPUT TYPE="text" NAME="svc" VALUE="<% $hashref->{svc} %>"></TD>
+%#<TR>
 
 <& /elements/tr-select-part_svc_class.html, curr_value=>$hashref->{classnum} &>
 
 <TR>
-  <TD ALIGN="right">Self-service access</TD>
+  <TH ALIGN="right">Self-service access</TD>
   <TD>
     <SELECT NAME="selfservice_access">
 % tie my %selfservice_access, 'Tie::IxHash', #false laziness w/browse/part_svc
@@ -172,12 +196,12 @@ window.onload = function() {
 
 
 <TR>
-  <TD ALIGN="right">Disable new orders</TD>
+  <TH ALIGN="right">Disable new orders</TD>
   <TD><INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<% $hashref->{disabled} eq 'Y' ? ' CHECKED' : '' %>></TD>
 </TR>
 
 <TR>
-  <TD ALIGN="right">Preserve this service on package cancellation</TD>
+  <TH ALIGN="right">Preserve this service on package cancellation</TD>
   <TD><INPUT TYPE="checkbox" NAME="preserve" VALUE="Y"<% $hashref->{'preserve'} eq 'Y' ? ' CHECKED' : '' %>>&nbsp;</TD>
 </TR>
 
@@ -240,12 +264,12 @@ my $widget = new HTML::Widgets::SelectLayers(
   #'selected_layer' => $p_svcdb,
   'selected_layer' => $hashref->{svcdb} || 'svc_acct',
   'options'        => \%svcdb,
-  'form_name'      => 'dummy',
+  'form_name'      => 'SvcEditMain',
   #'form_action'    => 'process/part_svc.cgi',
   'form_action'    => 'part_svc.cgi', #self
-  'form_elements'  => [qw( svc svcpart classnum selfservice_access
-                           disabled preserve
-                      )],
+#  'form_elements'  => [qw( svc svcpart classnum selfservice_access
+#                           disabled preserve
+#                      )],
   'html_between'   => $help,
   'layer_callback' => sub {
     include('elements/part_svc_column.html',
index fd12c61..60aaf74 100644 (file)
@@ -62,6 +62,8 @@ Example:
                       'fields' => [qw( fieldname fieldname2 )],
                     },
 
+   'process_locale' => 'fieldname', # update entries in the _msgcat table
+
    'process_upload' => {
                          'process'  => 'misc/mytable-import.html',
                           # fields to pass to the back end job, besides the 
@@ -363,12 +365,21 @@ foreach my $value ( @values ) {
 
   }
 
-  if ( !$error && $opt{'process_o2m'} ) {
-
-    my @process_o2m = ref($opt{'process_o2m'}) eq 'ARRAY'
-                           ? @{ $opt{'process_o2m'} }
-                           :  ( $opt{'process_o2m'} );
+  my @process_o2m;
+  if ( $opt{'process_o2m'} ) {
+    @process_o2m = ref($opt{'process_o2m'}) eq 'ARRAY'
+                        ? @{ $opt{'process_o2m'} }
+                        :  ( $opt{'process_o2m'} );
+  }
+  if ( $opt{'process_locale'} ) {
+    push @process_o2m,
+    {
+      'table'  => $table . '_msgcat',
+      'fields' => [ 'locale', $opt{'process_locale'} ],
+    };
+  }
 
+  if ( !$error ) {
 
     foreach my $process_o2m (@process_o2m) {
 
index b804202..c4d150b 100755 (executable)
@@ -9,6 +9,7 @@
               'edit_ext'          => 'cgi',
               'precheck_callback' => $precheck_callback,
               'args_callback'     => $args_callback,
+              'process_locale'    => 'pkg',
               'process_m2m'       => \@process_m2m,
               'process_o2m'       => \@process_o2m,
           )
@@ -310,10 +311,6 @@ foreach my $amount_param ( grep /^usagepricepart(\d+)_amount$/, $cgi->param ) {
 
 my @process_o2m = (
   {
-    'table'  => 'part_pkg_msgcat',
-    'fields' => [qw( locale pkg )],
-  },
-  {
     'table'  => 'part_pkg_usageprice',
     'fields' => [qw( price currency action target amount )],
 
index dc00a88..26b5294 100644 (file)
@@ -10,9 +10,12 @@ my $quotation = qsearchs( 'quotation' => {
   quotationnum => scalar( $cgi->param('quotationnum') ),
 } ) or die 'unknown quotationnum';
 
+my $params = {};
+$$params{'onhold'} = $cgi->param('onhold') ? 1 : 0;
+
 my $cust_main = $quotation->cust_main;
 if ( $cust_main ) {
-  my $error = $quotation->order;
+  my $error = $quotation->order(undef,$params);
   errorpage($error) if $error;
 
   #i should be part of the order transaction
@@ -20,7 +23,7 @@ if ( $cust_main ) {
   $quotation->replace;
 
 } else {
-  $cust_main = $quotation->convert_cust_main;
+  $cust_main = $quotation->convert_cust_main( $params );
   errorpage($cust_main) unless ref($cust_main);# eq 'FS::cust_main';
 }
 
index ca26c6c..7be5eab 100755 (executable)
@@ -31,7 +31,7 @@
 </TR>
 
 <% include('/elements/tr-td-label.html',
-     'label'    => mt('Username'),
+     'label'    => $part_svc->part_svc_column('username')->columnlabel || mt('Username'),
      'required' => $part_svc->part_svc_column('username')->required ) %>
 % if ( $svcnum && $conf->exists('svc_acct-no_edit_username') ) {
     <TD BGCOLOR="#eeeeee"><% $svc_acct->username() %></TD>
@@ -46,7 +46,7 @@
 %if ( $part_svc->part_svc_column('_password')->columnflag ne 'F' ) {
 % #XXX eventually should require "Edit Password" ACL
 <% include('/elements/tr-td-label.html',
-     'label'    => mt('Password'),
+     'label'    => $part_svc->part_svc_column('_password')->columnlabel || mt('Password'),
      'required' => $part_svc->part_svc_column('_password')->required ) %>
   <TD>
     <INPUT TYPE="text" ID="clear_password" NAME="clear_password" VALUE="<% $password %>" SIZE=<% $pmax2 %> MAXLENGTH=<% $pmax %>>
@@ -68,7 +68,7 @@
 %  && $part_svc->part_svc_column('sec_phrase')->columnflag ne 'F' ) {
 
 <% include('/elements/tr-td-label.html',
-     'label'    => mt('Security phrase'),
+     'label'    => $part_svc->part_svc_column('sec_phrase')->columnlabel || mt('Security phrase'),
      'required' => $part_svc->part_svc_column('sec_phrase')->required ) %>
     <TD>
       <INPUT TYPE="text" NAME="sec_phrase" VALUE="<% $sec_phrase %>" SIZE=32>
 %                );
 
 <% include('/elements/tr-td-label.html',
-     'label'    => mt('Domain'),
+     'label'    => $part_svc->part_svc_column('domsvc')->columnlabel || mt('Domain'),
      'required' => $part_svc->part_svc_column('domsvc')->required ) %>
     <TD>
       <SELECT NAME="domsvc" SIZE=1>
              'curr_value' => $svc_acct->pbxsvc,
              'part_svc'   => $part_svc,
              'cust_pkg'   => $cust_pkg,
+             'label'      => $part_svc->part_svc_column('pbxsvc')->columnlabel || 'PBX',
 &>
 
 %#pop
 % } else { 
 
 <% include('/elements/tr-td-label.html',
-     'label'    => mt('Access number'),
+     'label'    => $part_svc->part_svc_column('popnum')->columnlabel || mt('Access number'),
      'required' => $part_svc->part_svc_column('popnum')->required ) %>
     <TD><% FS::svc_acct_pop::popselector($popnum) %></TD>
   </TR>
          #'part_svc'   => $part_svc,
          #'cust_pkg'   => $cust_pkg,
          'required'   => $part_svc->part_svc_column('sectornum')->required,
+         'label'       => $part_svc->part_svc_column('sectornum')->columnlabel || mt('Tower sector'),
     &>
 %} else {
     <INPUT TYPE="hidden" NAME="sectornum" VALUE="<% $svc_acct->sectornum %>">
 % if ( length($svc_acct->$xid()) ) { 
 
 <% include('/elements/tr-td-label.html',
-     'label'    => uc($xid),
+     'label'    => $part_svc->part_svc_column($xid)->columnlabel || uc($xid),
      'required' => $part_svc->part_svc_column($xid)->required ) %>
-      <TR>
-        <TD ALIGN="right"><% uc($xid) %></TD>
+%#      <TR>
+%#        <TD ALIGN="right"><% uc($xid) %></TD>
           <TD BGCOLOR="#eeeeee"><% $svc_acct->$xid() %></TD>
         <TD>
         </TD>
 % } else { 
   
 <% include('/elements/tr-td-label.html',
-     'label'    => uc($xid),
+     'label'    => $part_svc->part_svc_column($xid)->columnlabel || uc($xid),
      'required' => $part_svc->part_svc_column($xid)->required ) %>
       <TD>
         <INPUT TYPE="text" NAME="<% $xid %>" SIZE=8 MAXLENGTH=6 VALUE="<% $svc_acct->$xid() %>">
 
 
 <% include('/elements/tr-td-label.html',
-     'label'    => mt('Real Name'),
+     'label'    => $part_svc->part_svc_column('finger')->columnlabel || mt('Real Name'),
      'required' => $part_svc->part_svc_column('finger')->required ) %>
     <TD>
       <INPUT TYPE="text" NAME="finger" VALUE="<% $svc_acct->finger %>">
 
 
 <% include('/elements/tr-td-label.html',
-     'label'    => mt('Home directory'),
+     'label'    => $part_svc->part_svc_column('dir')->columnlabel || mt('Home directory'),
      'required' => $part_svc->part_svc_column('dir')->required ) %>
     <TD><INPUT TYPE="text" NAME="dir" VALUE="<% $svc_acct->dir %>"></TD>
   </TR>
 
 
 <% include('/elements/tr-td-label.html',
-     'label'    => mt('Shell'),
+     'label'    => $part_svc->part_svc_column('shell')->columnlabel || mt('Shell'),
      'required' => $part_svc->part_svc_column('shell')->required ) %>
     <TD>
       <SELECT NAME="shell" SIZE=1>
   'object' => $svc_acct,
   'ip_field' => 'slipip',
   'required' => $part_svc->part_svc_column('routernum')->required,
+  'label'    => $part_svc->part_svc_column('routernum')->columnlabel,
   'ip_addr_required' => $part_svc->part_svc_column('slipip')->required,
+  'ip_addr_label' => $part_svc->part_svc_column('slipip')->columnlabel,
 &>
 % } else {
 %   # don't expose these to the user--they're only useful in the other case
     <INPUT TYPE="hidden" NAME="slipip" VALUE="<% $svc_acct->slipip %>">
 %   } else { 
 <% include('/elements/tr-td-label.html',
-     'label'    => mt('IP'),
+     'label'    => $part_svc->part_svc_column('slipip')->columnlabel || mt('IP'),
      'required' => $part_svc->part_svc_column('slipip')->required ) %>
       <TD><INPUT TYPE="text" NAME="slipip" VALUE="<% $svc_acct->slipip %>"></TD>
     </TR>
 
 
 <% include('/elements/tr-td-label.html',
-     'label'    => mt('RADIUS groups'),
+     'label'    => $part_svc->part_svc_column('usergroup')->columnlabel || mt('RADIUS groups'),
      'required' => $part_svc->part_svc_column('usergroup')->required ) %>
 % if ( $part_svc_usergroup->columnflag eq 'F' ) { 
     <TD BGCOLOR="#eeeeee"><% join('<BR>', @groupnames) %></TD>
index 370bfb0..544d00b 100644 (file)
@@ -46,7 +46,7 @@
 % if ( $part_svc->part_svc_column('quota')->columnflag eq 'F' ) { 
   <INPUT TYPE="hidden" NAME="quota" VALUE="<% $svc_acct->quota %>">
 % } else {
-%   my $quota_label = $communigate ? 'Mail storage limit' : 'Quota';
+%   my $quota_label = $communigate ? 'Mail storage limit' : ($part_svc->part_svc_column('quota')->columnlabel || 'Quota');
 <% include('/elements/tr-td-label.html',
      'label'    => $quota_label,
      'required' => $part_svc->part_svc_column('quota')->required ) %>
index fb5e7d9..cc104a1 100644 (file)
@@ -235,7 +235,7 @@ div.fstabcontainer {
 .fsinnerbox th {
   font-weight:normal;
   font-size:80%;
-  valign: bottom;
+  vertical-align: bottom;
   color: #666666;
 }
 
index db38eaf..07595a5 100644 (file)
@@ -47,6 +47,9 @@ Example:
       <link rel="stylesheet" href="<% $fsurl %>elements/jquery-ui.min.css">
       <SCRIPT SRC="<% $fsurl %>elements/jquery.js"></SCRIPT>
       <SCRIPT SRC="<% $fsurl %>elements/jquery-ui.min.js"></SCRIPT>
+%     if ( $FS::CurrentUser::CurrentUser->option('printtofit') ) {
+      <SCRIPT SRC="<% $fsurl %>elements/printtofit.js"></SCRIPT>
+%     }
 %   }
     <% include('init_overlib.html') |n %>
     <% include('rs_init_object.html') |n %>
index 1759369..6c0f80b 100644 (file)
@@ -30,6 +30,9 @@ Example:
     <META HTTP-Equiv="Expires" Content="0"> 
 %   unless ( $no_jquery ) {
       <SCRIPT SRC="<% $fsurl %>elements/jquery.js"></SCRIPT>
+%     if ( $FS::CurrentUser::CurrentUser->option('printtofit') ) {
+      <SCRIPT SRC="<% $fsurl %>elements/printtofit.js"></SCRIPT>
+%     }
 %   }
     <% $head |n %>
   </HEAD>
diff --git a/httemplate/elements/printtofit.js b/httemplate/elements/printtofit.js
new file mode 100644 (file)
index 0000000..66257fc
--- /dev/null
@@ -0,0 +1,26 @@
+$().ready(function() {
+  var beforePrint = function() {
+    if ($('body').width() > 0) {
+      // 7.5 inches * 96 DPI; maybe make the width a user pref?
+      var maxwidth = 7.5 * 96;
+      $('body').css('zoom', maxwidth / $('body').width());
+    }
+  };
+  var afterPrint = function() {
+    $('body').css('zoom', 1);
+  }
+
+  if (window.matchMedia) { // chrome, most importantly; also IE10?
+    window.matchMedia('print').addListener(
+      function(mq) {
+        mq.matches ?  beforePrint() : afterPrint();
+      }
+    );
+  } else { // other IE
+    $(window).on('beforeprint', beforePrint);
+    $(window).on('afterprint', afterPrint);
+  }
+  // got nothing for firefox
+  // https://bugzilla.mozilla.org/show_bug.cgi?id=774398
+  // but firefox already has "shrink to fit"
+});
index e38dde6..0c2b816 100644 (file)
@@ -98,14 +98,14 @@ function <%$key%>process () {
 
   overlib( 'Submitting job to server...', WIDTH, 444, HEIGHT, 168, CAPTION, 'Please wait...', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', CLOSECLICK, MIDX, 0, MIDY, 0 );
 
+  // jQuery .serializeArray() maybe?
+  var copy_fields = <% encode_json(\%copy_fields) %>;
   var Hash = new Array();
   var x = 0;
   var fieldName;
   for (var i = 0; i<document.<%$formname%>.elements.length; i++) {
     field  = document.<%$formname%>.elements[i];
-    if ( <% join(' || ', map { "(field.name.indexOf('$_') > -1)" } @$fields ) %>
-       )
-    {
+    if ( <% $all_fields %> || copy_fields[ field.name ] ) {
         if ( field.type == 'select-multiple' ) {
           //alert('select-multiple ' + field.name);
           for (var j=0; j < field.options.length; j++) {
@@ -168,6 +168,14 @@ $progress_url->query_form(
   %dest_info,
 );
 
+my $all_fields = 0;
+my %copy_fields;
+if (grep '/^ALL$/', @$fields) {
+  $all_fields = 1;
+} else {
+  %copy_fields = map { $_ => 1 } @$fields;
+}
+
 #stupid safari is caching the "location" of popup iframs, and submitting them
 #instead of displaying them.  this should prevent that.
 my $popup_name = 'popup-'.random_id();
index 85758d5..488acca 100644 (file)
@@ -1,31 +1,27 @@
-<SELECT NAME="<% $opt{name} %>">
+<SELECT NAME="<% $opt{'name'} %>"<% $opt{'multiple'} ? ' MULTIPLE' : '' %>>
 % while ( @fields ) {
-<OPTION VALUE="<% shift @fields %>"><% shift @fields %></OPTION>
+%   my $value = shift @fields;
+%   my $label = shift @fields;
+<OPTION VALUE="<% $value %>"<% $curr_value{$value} ? ' SELECTED' : '' %>><% $label %></OPTION>
 % }
 </SELECT>
 <%init>
 my %opt = @_;
-my $lookuptype = $opt{lookuptype};
-my $valuetype = $opt{valuetype};
-# get a list of TimeValue-type custom fields
-my $CurrentUser = RT::CurrentUser->new();
-$CurrentUser->LoadByName($FS::CurrentUser::CurrentUser->username);
-die "RT not configured" unless $CurrentUser->id;
-my $CFs = RT::CustomFields->new($CurrentUser);
 
-$CFs->Limit(FIELD => 'LookupType',
-            OPERATOR => 'ENDSWITH',
-            VALUE => $lookuptype)
-    if $lookuptype;
-
-$CFs->Limit(FIELD => 'Type',
-            VALUE => $valuetype)
-    if $valuetype;
+my %curr_value = map { $_ => 1 } split(', ',$opt{'curr_value'});
 
 my @fields;
 push @fields, '', $opt{empty_label} if exists($opt{empty_label});
 
-while (my $CF = $CFs->Next) {
-  push @fields, $CF->Name, ($CF->Description || $CF->Name);
+my $conf = new FS::Conf;
+
+if ($conf->config('ticket_system') eq 'RT_Internal') {
+
+  push @fields, FS::TicketSystem->custom_fields(
+    lookuptype => $opt{lookuptype},
+    valuetype  => $opt{valuetype},
+  );
+
 }
+
 </%init>
diff --git a/httemplate/elements/select-rt-queue.html b/httemplate/elements/select-rt-queue.html
new file mode 100644 (file)
index 0000000..4ae8bc9
--- /dev/null
@@ -0,0 +1,24 @@
+<SELECT NAME="<% $opt{'name'} %>"<% $opt{'multiple'} ? ' MULTIPLE' : '' %>>
+% while ( @fields ) {
+%   my $value = shift @fields;
+%   my $label = shift @fields;
+<OPTION VALUE="<% $value %>"<% $curr_value{$value} ? ' SELECTED' : '' %>><% $label %></OPTION>
+% }
+</SELECT>
+<%init>
+my %opt = @_;
+
+my %curr_value = map { $_ => 1 } split(', ',$opt{'curr_value'});
+
+my @fields;
+push @fields, '', $opt{empty_label} if exists($opt{empty_label});
+
+my $conf = new FS::Conf;
+
+if ($conf->config('ticket_system') eq 'RT_Internal') {
+
+  push @fields, FS::TicketSystem->queues();
+
+}
+
+</%init>
index a330df1..eda439a 100644 (file)
@@ -34,10 +34,7 @@ my $empty_label =
 
 my $empty_value = $opt{'empty_value'} || '';
 
-my @terms = ( emt('Payable upon receipt'),
-              ( map "Net $_",
-                0, 3, 5, 7, 9, 10, 14, 15, 18, 20, 21, 25, 30, 45, 60, 90 ),
-            );
+my @terms = map emt($_), @FS::Conf::invoice_terms;
 
 my @pre_options = $opt{pre_options} ? @{ $opt{pre_options} } : ();
 
diff --git a/httemplate/elements/tr-input-locale-text.html b/httemplate/elements/tr-input-locale-text.html
new file mode 100644 (file)
index 0000000..110a8aa
--- /dev/null
@@ -0,0 +1,120 @@
+<%doc>
+Usage:
+
+In edit/foo.html:
+
+<& /elements/tr-input-locale-text.html,
+  cgi     => $cgi, # needed to preserve values in error redirect
+  object  => $record,
+  field   => 'myfield',
+  label   => 'My Field',
+&>
+
+And in edit/process/foo.html:
+<& elements/process.html,
+  ...
+  process_locale => 'myfield',
+&>
+
+'object' needs to be an FS::Record subclass instance for a table that has
+a '_msgcat' localization table. For a table "foo" where "foo.myfield"
+contains some customer-visible label (in the default locale),
+"foo_msgcat.myfield" contains the translation of that label for a customer
+locale. The foreign key in foo_msgcat must have the same name as the primary
+key of foo.
+
+Currently only a single field can be localized this way; including this
+element more than once in the form will lead to conflicts. This is how
+it should work; if at some point we need to localize several fields of the
+same record, we should modify this element to show multiple inputs for each
+locale.
+
+</%doc>
+<%init>
+
+my %opt = @_;
+my $object = delete $opt{object};
+my $field = delete $opt{field};
+
+# identify our locales
+my $conf = FS::Conf->new;
+my $default_locale = $conf->config('locale') || 'en_';
+my @locales = grep { ! /^$default_locale/ } $conf->config('available-locales');
+
+my $label = delete $opt{label};
+my %labels = map { $_ => "$label&mdash;".FS::Locales->description($_) }
+              @locales;
+@locales = sort { $labels{$a} cmp $labels{$b} } @locales;
+my %curr_values;
+
+# where are the msgcat records?
+my $msgcat_table = $object->table . '_msgcat';
+my $msgcat_pkey = dbdef->table($msgcat_table)->primary_key;
+my %msgcat_pkeyvals;
+
+# find existing msgcat records, if any, and record their message values
+# and pkeys
+my $pkey = $object->primary_key;
+my $pkeyval = $object->get($pkey);
+if ($pkeyval) { # of course if this is a new record there won't be any
+  my @linked = qsearch($msgcat_table, { $pkey => $pkeyval });
+  foreach (@linked) {
+    $curr_values{ $_->locale } = $_->get( $field );
+    $msgcat_pkeyvals{ $_->locale } = $_->get( $msgcat_pkey );
+  }
+}
+
+# sticky-on-error the locale inputs
+if( my $cgi = $opt{cgi} ) {
+  my $i = 0;
+  # they're named 'foomsgnum0_locale' and 'foomsgnum0_myfield'
+  while ( my $locale = $cgi->param($msgcat_pkey . $i . '_locale') ) {
+    my $value = $cgi->param($msgcat_pkey . $i . '_' . $field);
+    $curr_values{ $locale } = $value;
+    $i++;
+  }
+}
+
+# compat with tr-input-text for styling
+my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
+
+my $colspan = $opt{'colspan'} ? 'COLSPAN="'.$opt{'colspan'}.'"' : '';
+
+
+</%init>
+% # pass through %opt on all of these to retain formatting
+% # one tr, td, and input for the default locale
+<& tr-input-text.html,
+  %opt,
+  'label' => $label,
+  'field' => $field
+&>
+% # and one for each of the others 
+% my $i = 0;
+% foreach my $locale (@locales) {
+%   my $basename = $msgcat_pkey . $i;
+%   my $lfield = $basename . '_' . $field;
+<& tr-td-label.html,
+  %opt,
+  'id' => $lfield, # uniqueness
+  'label' => $labels{$locale}
+&>
+  <TD <% $colspan %><% $cell_style %> ID="<% $lfield %>_input0">
+    <& hidden.html,
+      'field' => $basename,
+      'curr_value' => $msgcat_pkeyvals{$locale},
+      # will be empty if this is a new record and/or new locale, that's fine
+    &>
+    <& hidden.html,
+      'field' => $basename . '_locale',
+      'curr_value' => $locale,
+    &>
+    <& input-text.html,
+      %opt,
+      'field' => $lfield,
+      'curr_value' => $curr_values{$locale},
+    &>
+  </TD>
+</TR>
+%   $i++;
+% } # foreach $locale
index ee13568..2aa715e 100644 (file)
@@ -56,7 +56,7 @@ function clearhint_ip_addr (what) {
 ]
 &>
 </td></tr>
-<& /elements/tr-td-label.html, label => 'IP address', required => $opt{'ip_addr_required'} &>
+<& /elements/tr-td-label.html, label => ($opt{'ip_addr_label'} || 'IP address'), required => $opt{'ip_addr_required'} &>
 <td>
 % #warn Dumper \%fixed;
 % if ( exists $fixed{$ip_field} ) {
index 2f4f0d5..e708711 100644 (file)
@@ -44,7 +44,7 @@ my %initialized = ();#won't work if component is "preloaded"... so don't do that
             len = args.length - 1;
         }
         for (var i = 0; i < len; i++) 
-            content = content + "&arg=" + escape(args[i]);
+            content = content + "&arg=" + encodeURIComponent(args[i]);
         content = content.replace( /[+]/g, '%2B'); // fix unescaped plus signs 
 
         if ( '<%$method%>' == 'GET' ) {
index f03a8df..b622efc 100644 (file)
@@ -55,6 +55,7 @@ unless ( $error ) { # if ($access_user) {
                       disable_html_editor disable_enter_submit_onetimecharge
                       enable_mask_clipboard_hack dashboard_customers
                       customer_view_emails
+                      printtofit
                       email_address
                       snom-ip snom-username snom-password
                       vonage-fromnumber vonage-username vonage-password
index 50d6e8d..c6bcf6f 100644 (file)
@@ -130,6 +130,13 @@ Interface
   </TR>
 
   <TR>
+    <TH ALIGN="right">Scale documents to fit on a letter-size page</TH>
+    <TD ALIGN="left">
+      <INPUT TYPE="checkbox" NAME="printtofit" VALUE="Y" <% $curuser->option('printtofit') ? 'CHECKED' : '' %>>
+    </TD>
+  </TR>
+
+  <TR>
     <TH ALIGN="right">How many recently-modified customers displayed on dashboard</TH>
     <TD ALIGN="left" COLSPAN=2>
       <INPUT TYPE="text" NAME="dashboard_customers" VALUE="<% $curuser->option('dashboard_customers') %>"></TD>
@@ -143,8 +150,6 @@ Interface
     </TD>
   </TR>
 
-
-
 </TABLE>
 <BR>
 
index 4ed297d..03aaedd 100755 (executable)
@@ -67,6 +67,15 @@ Examples:
                                     ],
                 'show_combined'  => 1,
 &>
+<%shared>
+# canonicalize the payby subtype string to an SQL-quoted list
+my %cardtype_of = (
+  'VisaMC'    => q['VISA card', 'MasterCard'],
+  'Amex'      => q['American Express card'],
+  'Discover'  => q['Discover card'],
+  'Maestro'   => q['Switch', 'Solo', 'Laser'],
+);  
+</%shared>
 <%init>
 
 my %opt = @_;
@@ -191,10 +200,8 @@ if ($opt{'show_card_type'}) {
   push @header, emt('Card Type');
   $align .= 'r';
   push @links, '';
-  push @fields, sub { 
-    (($_[0]->payby eq 'CARD') && ($_[0]->paymask !~ /N\/A/)) ? cardtype($_[0]->paymask) : ''
-  };
-  push @sort_fields, '';
+  push @fields, 'paycardtype';
+  push @sort_fields, 'paycardtype';
 }
 
 if ( $unapplied ) {
@@ -305,150 +312,32 @@ if ( $cgi->param('magic') ) {
     if ( $cgi->param('payby') ) {
 
       my @all_payby_search = ();
-      foreach my $payby ( $cgi->param('payby') ) {
-
-        $payby =~
-          /^(CARD|CHEK|BILL|CASH|PPAL|APPL|ANRD|PREP|WIRE|WEST|IDTP|EDI|MCRD|MCHK)(-(VisaMC|Amex|Discover|Maestro|Tokenized))?$/
-            or die "illegal payby $payby";
-
-        my $payby_search = "$table.payby = '$1'";
-
-        if ( $3 ) {
-
-          my $cardtype = $3;
-
-          my $similar_to = dbh->{Driver}->{Name} =~ /^mysql/i
-                             ? 'REGEXP' #doesn't behave exactly the same, but
-                                        #should work for our patterns
-                             : 'SIMILAR TO';
-
-          my $search;
-          if ( $cardtype eq 'VisaMC' ) {
-
-            #avoid posix regexes for portability
-            $search =
-              # Visa
-              " ( (     substring($table.payinfo from 1 for 1) = '4'     ".
-              #   is not Switch
-              "     AND substring($table.payinfo from 1 for 4) != '4936' ".
-              "     AND substring($table.payinfo from 1 for 6)           ".
-              "         NOT $similar_to '49030[2-9]'                        ".
-              "     AND substring($table.payinfo from 1 for 6)           ".
-              "         NOT $similar_to '49033[5-9]'                        ".
-              "     AND substring($table.payinfo from 1 for 6)           ".
-              "         NOT $similar_to '49110[1-2]'                        ".
-              "     AND substring($table.payinfo from 1 for 6)           ".
-              "         NOT $similar_to '49117[4-9]'                        ".
-              "     AND substring($table.payinfo from 1 for 6)           ".
-              "         NOT $similar_to '49118[1-2]'                        ".
-              "   )".
-              # MasterCard
-              "   OR substring($table.payinfo from 1 for 2) = '51' ".
-              "   OR substring($table.payinfo from 1 for 2) = '52' ".
-              "   OR substring($table.payinfo from 1 for 2) = '53' ".
-              "   OR substring($table.payinfo from 1 for 2) = '54' ".
-              "   OR substring($table.payinfo from 1 for 2) = '54' ".
-              "   OR substring($table.payinfo from 1 for 2) = '55' ".
-              "   OR substring($table.payinfo from 1 for 4) $similar_to '222[1-9]' ".
-              "   OR substring($table.payinfo from 1 for 3) $similar_to '22[3-9]' ".
-              "   OR substring($table.payinfo from 1 for 2) $similar_to '2[3-6]' ".
-              "   OR substring($table.payinfo from 1 for 3) $similar_to '27[0-1]' ".
-              "   OR substring($table.payinfo from 1 for 4) = '2720' ".
-              "   OR substring($table.payinfo from 1 for 3) = '2[2-7]x' ".
-              " ) ";
-
-          } elsif ( $cardtype eq 'Amex' ) {
-
-            $search =
-              " (    substring($table.payinfo from 1 for 2 ) = '34' ".
-              "   OR substring($table.payinfo from 1 for 2 ) = '37' ".
-              " ) ";
-
-          } elsif ( $cardtype eq 'Discover' ) {
-
-            my $country = $conf->config('countrydefault') || 'US';
-
-            $search =
-              " (    substring($table.payinfo from 1 for 4 ) = '6011'  ".
-              "   OR substring($table.payinfo from 1 for 3 ) = '60x'   ".
-              "   OR substring($table.payinfo from 1 for 2 ) = '65'    ".
-
-              # diner's 300-305 / 3095
-              "   OR substring($table.payinfo from 1 for 3 ) = '300'   ".
-              "   OR substring($table.payinfo from 1 for 3 ) = '301'   ".
-              "   OR substring($table.payinfo from 1 for 3 ) = '302'   ".
-              "   OR substring($table.payinfo from 1 for 3 ) = '303'   ".
-              "   OR substring($table.payinfo from 1 for 3 ) = '304'   ".
-              "   OR substring($table.payinfo from 1 for 3 ) = '305'   ".
-              "   OR substring($table.payinfo from 1 for 4 ) = '3095'  ".
-              "   OR substring($table.payinfo from 1 for 3 ) = '30x'   ".
-
-              # diner's 36, 38, 39
-              "   OR substring($table.payinfo from 1 for 2 ) = '36'    ".
-              "   OR substring($table.payinfo from 1 for 2 ) = '38'    ".
-              "   OR substring($table.payinfo from 1 for 2 ) = '39'    ".
-
-              "   OR substring($table.payinfo from 1 for 3 ) = '644'   ".
-              "   OR substring($table.payinfo from 1 for 3 ) = '645'   ".
-              "   OR substring($table.payinfo from 1 for 3 ) = '646'   ".
-              "   OR substring($table.payinfo from 1 for 3 ) = '647'   ".
-              "   OR substring($table.payinfo from 1 for 3 ) = '648'   ".
-              "   OR substring($table.payinfo from 1 for 3 ) = '649'   ".
-              "   OR substring($table.payinfo from 1 for 3 ) = '64x'   ".
-
-              # JCB cards in the 3528-3589 range identified as Discover inside US & territories (NOT Canada)
-              ( $country =~ /^(US|PR|VI|MP|PW|GU)$/
-               ?" OR substring($table.payinfo from 1 for 4 ) = '3528'  ".
-                " OR substring($table.payinfo from 1 for 4 ) = '3529'  ".
-                " OR substring($table.payinfo from 1 for 3 ) = '353'   ".
-                " OR substring($table.payinfo from 1 for 3 ) = '354'   ".
-                " OR substring($table.payinfo from 1 for 3 ) = '355'   ".
-                " OR substring($table.payinfo from 1 for 3 ) = '356'   ".
-                " OR substring($table.payinfo from 1 for 3 ) = '357'   ".
-                " OR substring($table.payinfo from 1 for 3 ) = '358'   ".
-                " OR substring($table.payinfo from 1 for 3 ) = '35x'   "
-               :""
-              ).
-
-              #China Union Pay processed as Discover in US, Mexico and Caribbean
-              ( $country =~ /^(US|MX|AI|AG|AW|BS|BB|BM|BQ|VG|KY|CW|DM|DO|GD|GP|JM|MQ|MS|BL|KN|LC|VC|MF|SX|TT|TC)$/
-               ?" OR substring($table.payinfo from 1 for 3 ) $similar_to '62[24-68x]'   "
-               :""
-              ).
-
-              " ) ";
-
-          } elsif ( $cardtype eq 'Maestro' ) {
-
-            $search =
-              " (    substring($table.payinfo from 1 for 2 ) = '63'     ".
-              "   OR substring($table.payinfo from 1 for 2 ) = '67'     ".
-              "   OR substring($table.payinfo from 1 for 6 ) = '564182' ".
-              "   OR substring($table.payinfo from 1 for 4 ) = '4936'   ".
-              "   OR substring($table.payinfo from 1 for 6 )            ".
-              "      $similar_to '49030[2-9]'                             ".
-              "   OR substring($table.payinfo from 1 for 6 )            ".
-              "      $similar_to '49033[5-9]'                             ".
-              "   OR substring($table.payinfo from 1 for 6 )            ".
-              "      $similar_to '49110[1-2]'                             ".
-              "   OR substring($table.payinfo from 1 for 6 )            ".
-              "      $similar_to '49117[4-9]'                             ".
-              "   OR substring($table.payinfo from 1 for 6 )            ".
-              "      $similar_to '49118[1-2]'                             ".
-              " ) ";
-
-          } elsif ( $cardtype eq 'Tokenized' ) {
-
-            $search = " substring($table.payinfo from 1 for 2 ) = '99' ";
+      foreach my $payby_string ( $cgi->param('payby') ) {
+
+        my $payby_search;
+
+        my ($payby, $subtype) = split('-', $payby_string);
+        # make sure it exists and is a transaction type
+        if ( FS::payby->payment_payby2longname($payby) ) {
+          $payby_search = "$table.payby = " . dbh->quote($payby);
+        } else {
+          die "illegal payby $payby_string";
+        }
+
+        if ( $subtype ) {
+
+          if ( $subtype eq 'Tokenized' ) {
+
+            $payby_search .= " AND substring($table.payinfo from 1 for 2 ) = '99' ";
+            # XXX should store the cardtype as 'Tokenized' in this case?
 
           } else {
-            die "unknown card type $cardtype";
-          }
 
-          my $masksearch = $search;
-          $masksearch =~ s/$table\.payinfo/$table.paymask/gi;
+            my $in_cardtype = $cardtype_of{$subtype}
+              or die "unknown card type $subtype";
+            $payby_search .= " AND $table.paycardtype IN($in_cardtype)";
 
-          $payby_search = "( $payby_search AND ( $search OR ( $table.paymask IS NOT NULL AND $masksearch ) ) )";
+          }
 
         }
 
@@ -610,6 +499,8 @@ if ( $cgi->param('magic') ) {
     'addl_from' => $addl_from,
   };
 
+warn Dumper \$sql_query;
+
 } else {
 
   #hmm... is this still used?
index 7221d53..58d398c 100755 (executable)
@@ -67,7 +67,9 @@ function areyousure(href, message) {
     <BR><BR>
 
 %   if ( $curuser->access_right('New customer') && $quotation->quotation_pkg ) {
+%     # if we end up with more than one option, combine these links and add an interstitial screen
       <A HREF="<%$p%>edit/process/quotation_convert.html?quotationnum=<% $quotation->quotationnum %>">Place order</A>
+      | <A HREF="<%$p%>edit/process/quotation_convert.html?quotationnum=<% $quotation->quotationnum %>&onhold=1">Order on hold</A>
       <BR><BR>
 %   }
 
index 1b7d56b..5532c22 100644 (file)
@@ -1,18 +1,18 @@
 <% &ntable("#cccccc") %><TR><TD><% &ntable("#cccccc",2) %>
 
-<& /view/elements/tr.html, label=>mt('Service'),  value=>$part_svc->svc &>
+<& /view/elements/tr.html, label=> $part_svc->part_svc_column('svc')->columnlabel || mt('Service'),  value=>$part_svc->svc &>
 % if ( $opt{cust_svc}->agent_svcid ) {
   <& /view/elements/tr.html, label=>mt('Legacy ID'),  value=>$opt{cust_svc}->agent_svcid &>
 % }
-<& /view/elements/tr.html, label=>mt('Username'), value=>$svc_acct->username &>
-<& /view/elements/tr.html, label=>mt('Domain'),   value=>$domain &>
+<& /view/elements/tr.html, label=> $part_svc->part_svc_column('username')->columnlabel || mt('Username'), value=>$svc_acct->username &>
+<& /view/elements/tr.html, label=> $part_svc->part_svc_column('domsvc')->columnlabel || mt('Domain'),   value=>$domain &>
 
 % if ( $opt{'communigate'} ) {
   <& /view/elements/tr.html, label=>mt('Aliases'), value=>$svc_acct->cgp_aliases &>
 %}
 
 % if ( $svc_acct->pbxsvc ) {
-  <& /view/elements/tr.html, label=>mt('PBX'), value=>$svc_acct->pbx_title &>
+  <& /view/elements/tr.html, label=> $part_svc->part_svc_column('pbxsvc')->columnlabel || mt('PBX'), value=>$svc_acct->pbx_title &>
 %}
 
 % my $show_pw = '';
@@ -38,7 +38,7 @@
 %   # show nothing
 % } else {
 <TR>
-  <TD ALIGN="right"><% mt('Password') %></TD>
+  <TD ALIGN="right"><% $psc->columnlabel || mt('Password') %></TD>
   <TD STYLE="background-color: #ffffff; white-space: nowrap">
   <% $show_pw %>
 %   my $curuser = $FS::CurrentUser::CurrentUser;
 % }
 
 % if ( $conf->exists('security_phrase') ) {
-  <& /view/elements/tr.html, label=>mt('Security phrase'), value=>$svc_acct->sec_phrase &>
+  <& /view/elements/tr.html, label=> $part_svc->part_svc_column('sec_phrase')->columnlabel || mt('Security phrase'), value=>$svc_acct->sec_phrase &>
 % } 
 
 % if ( $svc_acct->popnum ) {
 %   my $svc_acct_pop = qsearchs('svc_acct_pop',{'popnum'=>$svc_acct->popnum});
-    <& /view/elements/tr.html, label=>mt('Access number'), value=>$svc_acct_pop->text &>
+    <& /view/elements/tr.html, label=> $part_svc->part_svc_column('popnum')->columnlabel || mt('Access number'), value=>$svc_acct_pop->text &>
 % } 
 
 % if ( $svc_acct->sectornum && $conf->exists('svc_acct-tower_sector') ) {
@@ -72,7 +72,7 @@
 %                ? '<A HREF="http://'. $tower_sector->ip_addr. '">'
 %                : '';
     <& /view/elements/tr.html,
-        label => mt('Tower sector'),
+        label =>  $part_svc->part_svc_column('sectornum')->columnlabel || mt('Tower sector'),
         value => $link. $tower_sector->description. ($link ? '</A>' : ''),
     &>
 % }
 &>
 
 % if ($svc_acct->uid ne '') { 
-  <& /view/elements/tr.html, label=>mt('UID'), value=>$svc_acct->uid &>
+  <& /view/elements/tr.html, label=> $part_svc->part_svc_column('uid')->columnlabel || mt('UID'), value=>$svc_acct->uid &>
 % } 
 
 % if ($svc_acct->gid ne '') { 
-  <& /view/elements/tr.html, label=>mt('GID'), value=>$svc_acct->gid &>
+  <& /view/elements/tr.html, label=> $part_svc->part_svc_column('gid')->columnlabel || mt('GID'), value=>$svc_acct->gid &>
 % } 
 
 % if ($svc_acct->finger ne '') { 
-  <& /view/elements/tr.html, label=>mt('Real Name'), value=>$svc_acct->finger &>
+  <& /view/elements/tr.html, label=> $part_svc->part_svc_column('finger')->columnlabel || mt('Real Name'), value=>$svc_acct->finger &>
 % } 
 
 % if ($svc_acct->dir ne '') { 
-  <& /view/elements/tr.html, label=>mt('Home directory'), value=>$svc_acct->dir &>
+  <& /view/elements/tr.html, label=> $part_svc->part_svc_column('dir')->columnlabel || mt('Home directory'), value=>$svc_acct->dir &>
 % } 
 
 % if ($svc_acct->shell ne '') { 
-  <& /view/elements/tr.html, label=>mt('Shell'), value=>$svc_acct->shell &>
+  <& /view/elements/tr.html, label=> $part_svc->part_svc_column('shell')->columnlabel || mt('Shell'), value=>$svc_acct->shell &>
 % } 
 
 % if ($svc_acct->quota ne '' && ! $opt{'communigate'} ) { 
 
-  <& /view/elements/tr.html, label=>mt('Quota'), value=>$svc_acct->quota &>
+  <& /view/elements/tr.html, label=> $part_svc->part_svc_column('quota')->columnlabel || mt('Quota'), value=>$svc_acct->quota &>
 
 % } elsif ( $opt{'communigate'} ) {
 
@@ -133,7 +133,7 @@ sub slipip {
 
 % if ($svc_acct->slipip) { 
   <& /view/elements/tr.html,
-       label=>mt('IP address'),
+       label=> $part_svc->part_svc_column('slipip')->columnlabel || mt('IP address'),
        value=> slipip($svc_acct)
   &>
 % } 
@@ -156,7 +156,7 @@ sub slipip {
     &>
 % } 
 
-<& /view/elements/tr.html, label=>mt('RADIUS groups'),
+<& /view/elements/tr.html, label=> $part_svc->part_svc_column('usergroup')->columnlabel || mt('RADIUS groups'),
     value=>join('<BR>', $svc_acct->radius_groups('long_description')) &>
 
 <& router.html, 'svc_acct' => $svc_acct &>
index 7870d42..0328983 100644 (file)
@@ -38,18 +38,14 @@ sub Prepare  {
       VALUE => $TicketsObj->CurrentUser->id
     );
   }
-  $TicketsObj->Limit(
-    FIELD => 'Status',
-    OPERATOR => '!=',
-    ENTRYAGGREGATOR => 'AND',
-    VALUE => 'resolved'
-  );
-  $TicketsObj->Limit(
-    FIELD => 'Status',
-    OPERATOR => '!=',
-    ENTRYAGGREGATOR => 'AND',
-    VALUE => 'rejected',
-  );
+  foreach my $status (qw(resolved rejected deleted)) {
+    $TicketsObj->Limit(
+      FIELD => 'Status',
+      OPERATOR => '!=',
+      ENTRYAGGREGATOR => 'AND',
+      VALUE => $status,
+    );
+  }
   my $txn_alias = $TicketsObj->JoinTransactions;
   $TicketsObj->Limit(
     ALIAS => $txn_alias,