diff options
author | Ivan Kohler <ivan@freeside.biz> | 2016-07-20 13:02:15 -0700 |
---|---|---|
committer | Ivan Kohler <ivan@freeside.biz> | 2016-07-20 13:02:15 -0700 |
commit | c22d84e565ab16db142395dce2e8621624eff140 (patch) | |
tree | 3670e2bc0bf200910c3af24e5459e8b6966a992b /FS | |
parent | e9a7ae3aadab31f34c6bacb2376f817ecd4d7d8d (diff) | |
parent | f235a64b4e96e8d613fb3ecdd3acc7f65f9f291d (diff) |
Merge branch 'master' of git.freeside.biz:/home/git/freeside
Diffstat (limited to 'FS')
-rw-r--r-- | FS/FS/Conf.pm | 32 | ||||
-rw-r--r-- | FS/FS/Mason.pm | 1 | ||||
-rw-r--r-- | FS/FS/Schema.pm | 47 | ||||
-rw-r--r-- | FS/FS/Template_Mixin.pm | 16 | ||||
-rw-r--r-- | FS/FS/TicketSystem/RT_Internal.pm | 84 | ||||
-rw-r--r-- | FS/FS/Upgrade.pm | 11 | ||||
-rw-r--r-- | FS/FS/cust_location.pm | 9 | ||||
-rw-r--r-- | FS/FS/cust_main.pm | 49 | ||||
-rw-r--r-- | FS/FS/cust_pay.pm | 10 | ||||
-rw-r--r-- | FS/FS/cust_pay_void.pm | 4 | ||||
-rw-r--r-- | FS/FS/cust_payby.pm | 54 | ||||
-rw-r--r-- | FS/FS/cust_pkg.pm | 56 | ||||
-rw-r--r-- | FS/FS/cust_pkg_reason.pm | 48 | ||||
-rw-r--r-- | FS/FS/cust_refund.pm | 7 | ||||
-rw-r--r-- | FS/FS/cust_svc.pm | 15 | ||||
-rw-r--r-- | FS/FS/h_cust_svc.pm | 41 | ||||
-rw-r--r-- | FS/FS/msg_template.pm | 1 | ||||
-rw-r--r-- | FS/FS/part_pkg.pm | 12 | ||||
-rw-r--r-- | FS/FS/part_pkg/rt_field.pm | 207 | ||||
-rw-r--r-- | FS/FS/part_svc.pm | 27 | ||||
-rw-r--r-- | FS/FS/part_svc_msgcat.pm | 131 | ||||
-rw-r--r-- | FS/FS/payinfo_Mixin.pm | 37 | ||||
-rw-r--r-- | FS/FS/quotation.pm | 30 | ||||
-rw-r--r-- | FS/FS/rt_field_charge.pm | 132 |
24 files changed, 980 insertions, 81 deletions
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index a0eaab5c7..14bc1dc11 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -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.', diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 847f18bcd..0257b045a 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -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 ) { diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index a50b551da..ac585108e 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -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' => { diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index 5153f87e8..c8ddffd79 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -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... diff --git a/FS/FS/TicketSystem/RT_Internal.pm b/FS/FS/TicketSystem/RT_Internal.pm index ffee484e9..99e7044fa 100644 --- a/FS/FS/TicketSystem/RT_Internal.pm +++ b/FS/FS/TicketSystem/RT_Internal.pm @@ -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; diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index a374d391d..6f14cd202 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -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' => [], diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm index 0dec065e6..90400984c 100644 --- a/FS/FS/cust_location.pm +++ b/FS/FS/cust_location.pm @@ -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; diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 3fb0a87fb..2af6a1f01 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -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; diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index 331a15623..e0a7143c4 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -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 { diff --git a/FS/FS/cust_pay_void.pm b/FS/FS/cust_pay_void.pm index 8d37a58b5..29540d1c6 100644 --- a/FS/FS/cust_pay_void.pm +++ b/FS/FS/cust_pay_void.pm @@ -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 diff --git a/FS/FS/cust_payby.pm b/FS/FS/cust_payby.pm index 62fa9be5f..e4a1d193c 100644 --- a/FS/FS/cust_payby.pm +++ b/FS/FS/cust_payby.pm @@ -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 diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 661625725..bbb281ade 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -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 ); diff --git a/FS/FS/cust_pkg_reason.pm b/FS/FS/cust_pkg_reason.pm index d11d05e95..29b4b0a91 100644 --- a/FS/FS/cust_pkg_reason.pm +++ b/FS/FS/cust_pkg_reason.pm @@ -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; diff --git a/FS/FS/cust_refund.pm b/FS/FS/cust_refund.pm index ced954036..4d2baa514 100644 --- a/FS/FS/cust_refund.pm +++ b/FS/FS/cust_refund.pm @@ -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 diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm index 3f7348321..08183b46c 100644 --- a/FS/FS/cust_svc.pm +++ b/FS/FS/cust_svc.pm @@ -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 diff --git a/FS/FS/h_cust_svc.pm b/FS/FS/h_cust_svc.pm index 7b565adde..89a4cd7d0 100644 --- a/FS/FS/h_cust_svc.pm +++ b/FS/FS/h_cust_svc.pm @@ -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 diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index 1dd48cc1a..b89071710 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -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 $@; diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index 709e137f3..92943f25c 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -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 index 000000000..657a8d72c --- /dev/null +++ b/FS/FS/part_pkg/rt_field.pm @@ -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; diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm index 621a55410..dcc78435b 100644 --- a/FS/FS/part_svc.pm +++ b/FS/FS/part_svc.pm @@ -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 index 000000000..6d69198ec --- /dev/null +++ b/FS/FS/part_svc_msgcat.pm @@ -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; + diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm index 41768189e..4f26e8c6f 100644 --- a/FS/FS/payinfo_Mixin.pm +++ b/FS/FS/payinfo_Mixin.pm @@ -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 diff --git a/FS/FS/quotation.pm b/FS/FS/quotation.pm index 054985390..c61e001c6 100644 --- a/FS/FS/quotation.pm +++ b/FS/FS/quotation.pm @@ -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 index 000000000..fb01f810e --- /dev/null +++ b/FS/FS/rt_field_charge.pm @@ -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; + |