From: Ivan Kohler Date: Wed, 20 Jul 2016 20:02:15 +0000 (-0700) Subject: Merge branch 'master' of git.freeside.biz:/home/git/freeside X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=c22d84e565ab16db142395dce2e8621624eff140;hp=e9a7ae3aadab31f34c6bacb2376f817ecd4d7d8d Merge branch 'master' of git.freeside.biz:/home/git/freeside --- 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), 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, reason - Text of the new reason. +=item reason - can be set to a cancellation reason (see L), 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) +reason - Text of the new reason. =item cust_pkg_reason - can be an arrayref of L objects for the individual packages, parallel to the C 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 for valid values) Payment Information (See L for data format) +=item paycardtype + +Credit card type, if appropriate; autodetected. + =item paymask Masked payinfo (See L 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 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 flag will be omitted. -Returns a list of lists, calling the label method for all (historical) services -(see L) 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) 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 for valid payby values) Payment Information (See L for data format) +=item paycardtype + +Detected credit card type, if appropriate; autodetected. + =item paymask Masked payinfo (See L 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