summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
authorIvan Kohler <ivan@freeside.biz>2016-07-20 13:02:15 -0700
committerIvan Kohler <ivan@freeside.biz>2016-07-20 13:02:15 -0700
commitc22d84e565ab16db142395dce2e8621624eff140 (patch)
tree3670e2bc0bf200910c3af24e5459e8b6966a992b /FS
parente9a7ae3aadab31f34c6bacb2376f817ecd4d7d8d (diff)
parentf235a64b4e96e8d613fb3ecdd3acc7f65f9f291d (diff)
Merge branch 'master' of git.freeside.biz:/home/git/freeside
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/Conf.pm32
-rw-r--r--FS/FS/Mason.pm1
-rw-r--r--FS/FS/Schema.pm47
-rw-r--r--FS/FS/Template_Mixin.pm16
-rw-r--r--FS/FS/TicketSystem/RT_Internal.pm84
-rw-r--r--FS/FS/Upgrade.pm11
-rw-r--r--FS/FS/cust_location.pm9
-rw-r--r--FS/FS/cust_main.pm49
-rw-r--r--FS/FS/cust_pay.pm10
-rw-r--r--FS/FS/cust_pay_void.pm4
-rw-r--r--FS/FS/cust_payby.pm54
-rw-r--r--FS/FS/cust_pkg.pm56
-rw-r--r--FS/FS/cust_pkg_reason.pm48
-rw-r--r--FS/FS/cust_refund.pm7
-rw-r--r--FS/FS/cust_svc.pm15
-rw-r--r--FS/FS/h_cust_svc.pm41
-rw-r--r--FS/FS/msg_template.pm1
-rw-r--r--FS/FS/part_pkg.pm12
-rw-r--r--FS/FS/part_pkg/rt_field.pm207
-rw-r--r--FS/FS/part_svc.pm27
-rw-r--r--FS/FS/part_svc_msgcat.pm131
-rw-r--r--FS/FS/payinfo_Mixin.pm37
-rw-r--r--FS/FS/quotation.pm30
-rw-r--r--FS/FS/rt_field_charge.pm132
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;
+