);
use strict;
-use vars qw( %plans $DEBUG $setup_hack $skip_pkg_svc_hack );
+use vars qw( %plans $DEBUG $setup_hack $skip_pkg_svc_hack
+ $cache_enabled %cache_link %cache_pkg_svc
+ );
use Carp qw(carp cluck confess);
use Scalar::Util qw( blessed );
use DateTime;
use FS::part_pkg_discount;
use FS::part_pkg_vendor;
use FS::part_pkg_currency;
+use FS::part_svc_link;
$DEBUG = 0;
+
$setup_hack = 0;
$skip_pkg_svc_hack = 0;
+$cache_enabled = 0;
+%cache_link = ();
+%cache_pkg_svc = ();
+
=head1 NAME
FS::part_pkg - Object methods for part_pkg objects
ordered. The package will not start billing or have a setup fee charged
until it is manually unsuspended.
+=item change_to_pkgpart - When this package is ordered, schedule a future
+package change. The 'expire_months' field will determine when the package
+change occurs.
+
+=item expire_months - Number of months until this package expires (or changes
+to another package).
+
+=item adjourn_months - Number of months until this package becomes suspended.
+
+=item contract_end_months - Number of months until the package's contract
+ends.
+
=back
=head1 METHODS
If I<pkg_svc> is set to a hashref with svcparts as keys and quantities as
values, appropriate FS::pkg_svc records will be inserted. I<hidden_svc> can
be set to a hashref of svcparts and flag values ('Y' or '') to set the
-'hidden' field in these records.
+'hidden' field in these records, and I<provision_hold> can be set similarly
+for the 'provision_hold' field in these records.
If I<primary_svc> is set to the svcpart of the primary service, the appropriate
FS::pkg_svc record will be updated.
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ if ( length($self->classnum) && $self->classnum !~ /^(\d+)$/ ) {
+ my $pkg_class = qsearchs('pkg_class', { 'classname' => $self->classnum } )
+ || new FS::pkg_class { classname => $self->classnum };
+ unless ( $pkg_class->classnum ) {
+ my $error = $pkg_class->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+ $self->classnum( $pkg_class->classnum );
+ }
+
warn " inserting part_pkg record" if $DEBUG;
my $error = $self->SUPER::insert( $options{options} );
if ( $error ) {
warn " inserting pkg_svc records" if $DEBUG;
my $pkg_svc = $options{'pkg_svc'} || {};
my $hidden_svc = $options{'hidden_svc'} || {};
+ my $provision_hold = $options{'provision_hold'} || {};
foreach my $part_svc ( qsearch('part_svc', {} ) ) {
my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
my $primary_svc =
'quantity' => $quantity,
'primary_svc' => $primary_svc,
'hidden' => $hidden_svc->{$part_svc->svcpart},
+ 'provision_hold' => $provision_hold->{$part_svc->svcpart},
} );
my $error = $pkg_svc->insert;
if ( $error ) {
}
}
+ my $error = $self->check_pkg_svc(%options);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
}
if ( $options{'cust_pkg'} ) {
Replaces OLD_RECORD with this one in the database. If there is an error,
returns the error, otherwise returns false.
-Currently available options are: I<pkg_svc>, I<hidden_svc>, I<primary_svc>
-and I<options>
+Currently available options are: I<pkg_svc>, I<hidden_svc>, I<primary_svc>,
+I<bulk_skip>, I<provision_hold> and I<options>
If I<pkg_svc> is set to a hashref with svcparts as keys and quantities as
values, the appropriate FS::pkg_svc records will be replaced. I<hidden_svc>
-can be set to a hashref of svcparts and flag values ('Y' or '') to set the
-'hidden' field in these records. I<bulk_skip> can be set to a hashref of
-svcparts and flag values ('Y' or '') to set the 'bulk_skip' field in those
-records.
+can be set to a hashref of svcparts and flag values ('Y' or '') to set the
+'hidden' field in these records. I<bulk_skip> and I<provision_hold> can be
+set to a hashref of svcparts and flag values ('Y' or '') to set the
+respective field in those records.
-If I<primary_svc> is set to the svcpart of the primary service, the appropriate
-FS::pkg_svc record will be updated.
+If I<primary_svc> is set to the svcpart of the primary service, the
+appropriate FS::pkg_svc record will be updated.
-If I<options> is set to a hashref, the appropriate FS::part_pkg_option records
-will be replaced.
+If I<options> is set to a hashref, the appropriate FS::part_pkg_option
+records will be replaced.
If I<part_pkg_currency> is set to a hashref of options (with the keys as
-option_CURRENCY), appropriate FS::part_pkg::currency records will be replaced.
+option_CURRENCY), appropriate FS::part_pkg::currency records will be
+replaced.
=cut
my $pkg_svc = $options->{'pkg_svc'};
my $hidden_svc = $options->{'hidden_svc'} || {};
my $bulk_skip = $options->{'bulk_skip'} || {};
+ my $provision_hold = $options->{'provision_hold'} || {};
if ( $pkg_svc ) { # if it wasn't passed, don't change existing pkg_svcs
+
foreach my $part_svc ( qsearch('part_svc', {} ) ) {
my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
my $hidden = $hidden_svc->{$part_svc->svcpart} || '';
my $bulk_skip = $bulk_skip->{$part_svc->svcpart} || '';
+ my $provision_hold = $provision_hold->{$part_svc->svcpart} || '';
my $primary_svc =
( defined($options->{'primary_svc'}) && $options->{'primary_svc'}
&& $options->{'primary_svc'} == $part_svc->svcpart
my $old_primary_svc = '';
my $old_hidden = '';
my $old_bulk_skip = '';
+ my $old_provision_hold = '';
if ( $old_pkg_svc ) {
$old_quantity = $old_pkg_svc->quantity;
$old_primary_svc = $old_pkg_svc->primary_svc
if $old_pkg_svc->dbdef_table->column('primary_svc'); # is this needed?
$old_hidden = $old_pkg_svc->hidden;
- $old_bulk_skip = $old_pkg_svc->old_bulk_skip;
+ $old_bulk_skip = $old_pkg_svc->old_bulk_skip; # should this just be bulk_skip?
+ $old_provision_hold = $old_pkg_svc->provision_hold;
}
next unless $old_quantity != $quantity
|| $old_primary_svc ne $primary_svc
|| $old_hidden ne $hidden
- || $old_bulk_skip ne $bulk_skip;
+ || $old_bulk_skip ne $bulk_skip
+ || $old_provision_hold ne $provision_hold;
my $new_pkg_svc = new FS::pkg_svc( {
'pkgsvcnum' => ( $old_pkg_svc ? $old_pkg_svc->pkgsvcnum : '' ),
'primary_svc' => $primary_svc,
'hidden' => $hidden,
'bulk_skip' => $bulk_skip,
+ 'provision_hold' => $provision_hold,
} );
my $error = $old_pkg_svc
? $new_pkg_svc->replace($old_pkg_svc)
return $error;
}
} #foreach $part_svc
+
+ my $error = $new->check_pkg_svc(%$options);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
} #if $options->{pkg_svc}
my @part_pkg_vendor = $old->part_pkg_vendor;
'';
}
+sub validate_number {
+ my ($option, $valref) = @_;
+
+ $$valref = 0 unless $$valref;
+ return "Invalid $option"
+ unless ($$valref) = ($$valref =~ /^\s*(\d+)\s*$/);
+ return '';
+}
+
+sub validate_number_blank {
+ my ($option, $valref) = @_;
+
+ if ($$valref) {
+ return "Invalid $option"
+ unless ($$valref) = ($$valref =~ /^\s*(\d+)\s*$/);
+ }
+ return '';
+}
+
=item check
Checks all fields to make sure this is a valid package definition. If
|| $self->ut_floatn('pay_weight')
|| $self->ut_floatn('credit_weight')
|| $self->ut_numbern('taxproductnum')
+ || $self->ut_numbern('units_taxproductnum')
|| $self->ut_foreign_keyn('classnum', 'pkg_class', 'classnum')
|| $self->ut_foreign_keyn('addon_classnum', 'pkg_class', 'classnum')
|| $self->ut_foreign_keyn('taxproductnum',
|| $self->ut_numbern('delay_start')
|| $self->ut_foreign_keyn('successor', 'part_pkg', 'pkgpart')
|| $self->ut_foreign_keyn('family_pkgpart', 'part_pkg', 'pkgpart')
+ || $self->ut_numbern('expire_months')
+ || $self->ut_numbern('adjourn_months')
+ || $self->ut_numbern('contract_end_months')
+ || $self->ut_numbern('change_to_pkgpart')
+ || $self->ut_foreign_keyn('change_to_pkgpart', 'part_pkg', 'pkgpart')
|| $self->ut_alphan('agent_pkgpartid')
|| $self->SUPER::check
;
'';
}
+=item check_options
+
+Pass an I<$options> hashref that contains the values to be
+inserted or updated for any FS::part_pkg::MODULE.pm.
+
+For each key in I<$options>, validates the value by calling
+the 'validate' subroutine defined for that option e.g.
+FS::part_pkg::MODULE::plan_info()->{$KEY}->{validate}. The
+option validation function is only called when the hashkey for
+that option exists in I<$options>.
+
+Then the module validation function is called, from
+FS::part_pkg::MODULE::plan_info()->{validate}
+
+Returns error message, or empty string if valid.
+
+Invoked by L</insert> and L</replace> via the equivalent
+methods in L<FS::option_Common>.
+
+=cut
+
+sub check_options {
+ my ($self,$options) = @_;
+ foreach my $option (keys %$options) {
+ if (exists $plans{ $self->plan }->{fields}->{$option}) {
+ if (exists($plans{$self->plan}->{fields}->{$option}->{'validate'})) {
+ # pass option name for use in error message
+ # pass a reference to the $options value, so it can be cleaned up
+ my $error = &{$plans{$self->plan}->{fields}->{$option}->{'validate'}}($option,\($options->{$option}));
+ return $error if $error;
+ }
+ } # else "option does not exist" error?
+ }
+ if (exists($plans{$self->plan}->{'validate'})) {
+ my $error = &{$plans{$self->plan}->{'validate'}}($options);
+ return $error if $error;
+ }
+ return '';
+}
+
+=item check_pkg_svc
+
+Checks pkg_svc records as a whole (for part_svc_link dependencies).
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub check_pkg_svc {
+ my( $self, %opt ) = @_;
+
+ my $agentnum = $self->agentnum;
+
+ my %pkg_svc = map { $_->svcpart => $_ } $self->pkg_svc;
+
+ foreach my $svcpart ( keys %pkg_svc ) {
+
+ foreach my $part_svc_link ( $self->part_svc_link(
+ 'src_svcpart' => $svcpart,
+ 'link_type' => 'part_pkg_restrict',
+ )
+ ) {
+
+ return $part_svc_link->dst_svc. ' must be included with '.
+ $part_svc_link->src_svc
+ unless $pkg_svc{ $part_svc_link->dst_svcpart };
+ }
+
+ }
+
+ return '' if $opt{part_pkg_restrict_soft_override};
+
+ foreach my $svcpart ( keys %pkg_svc ) {
+
+ foreach my $part_svc_link ( $self->part_svc_link(
+ 'src_svcpart' => $svcpart,
+ 'link_type' => 'part_pkg_restrict_soft',
+ )
+ ) {
+ return $part_svc_link->dst_svc. ' is suggested with '.
+ $part_svc_link->src_svc
+ unless $pkg_svc{ $part_svc_link->dst_svcpart };
+ }
+
+ }
+
+ '';
+}
+
+=item part_svc_link OPTION => VALUE ...
+
+Returns the service dependencies (see L<FS::part_svc_link>) for the given
+search options, taking into account this package definition's agent.
+
+Available options are any field in part_svc_link. Typically used options are
+src_svcpart and link_type.
+
+=cut
+
+sub part_svc_link {
+ FS::part_svc_link->by_agentnum( shift->agentnum, @_ );
+}
+
=item supersede OLD [, OPTION => VALUE ... ]
Inserts this package as a successor to the package OLD. All options are as
sub pkg_svc {
my $self = shift;
+ return @{ $cache_pkg_svc{$self->pkgpart} }
+ if $cache_enabled && $cache_pkg_svc{$self->pkgpart};
+
# #sort { $b->primary cmp $a->primary }
# grep { $_->quantity }
# qsearch( 'pkg_svc', { 'pkgpart' => $self->pkgpart } );
my $opt = ref($_[0]) ? $_[0] : { @_ };
- my %pkg_svc = map { $_->svcpart => $_ }
- grep { $_->quantity }
- qsearch( 'pkg_svc', { 'pkgpart' => $self->pkgpart } );
+ my %pkg_svc = map { $_->svcpart => $_ } $self->_pkg_svc;
unless ( $opt->{disable_linked} ) {
foreach my $dst_pkg ( map $_->dst_pkg, $self->svc_part_pkg_link ) {
- my @pkg_svc = grep { $_->quantity }
- qsearch( 'pkg_svc', { pkgpart=>$dst_pkg->pkgpart } );
+ my @pkg_svc = $dst_pkg->_pkg_svc;
foreach my $pkg_svc ( @pkg_svc ) {
if ( $pkg_svc{$pkg_svc->svcpart} ) {
my $quantity = $pkg_svc{$pkg_svc->svcpart}->quantity;
}
}
- values(%pkg_svc);
+ my @pkg_svc = values(%pkg_svc);
+
+ $cache_pkg_svc{$self->pkgpart} = \@pkg_svc if $cache_enabled;
+ @pkg_svc;
+
+}
+
+sub _pkg_svc {
+ my $self = shift;
+ grep { $_->quantity }
+ qsearch({
+ 'select' => 'pkg_svc.*, part_svc.*',
+ 'table' => 'pkg_svc',
+ 'addl_from' => 'LEFT JOIN part_svc USING ( svcpart )',
+ 'hashref' => { 'pkgpart' => $self->pkgpart },
+ });
}
=item svcpart [ SVCDB ]
my( $self, $opt, $ornull ) = @_;
#cache: was pulled up in the original part_pkg query
- if ( $opt =~ /^(setup|recur)_fee$/ && defined($self->hashref->{"_$opt"}) ) {
- return $self->hashref->{"_$opt"};
- }
+ return $self->hashref->{"_opt_$opt"}
+ if exists $self->hashref->{"_opt_$opt"};
- cluck "$self -> option: searching for $opt"
- if $DEBUG;
+ cluck "$self -> option: searching for $opt" if $DEBUG;
my $part_pkg_option =
qsearchs('part_pkg_option', {
pkgpart => $self->pkgpart,
my %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); }
split("\n", $self->get('plandata') );
return $plandata{$opt} if exists $plandata{$opt};
+
+ # check whether the option is defined in plan info (if so, don't warn)
+ if (exists $plans{ $self->plan }->{fields}->{$opt}) {
+ return '';
+ }
cluck "WARNING: (pkgpart ". $self->pkgpart. ") Package def option $opt ".
"not found in options or plandata!\n"
unless $ornull;
sub _part_pkg_link {
my( $self, $type ) = @_;
- qsearch({ table => 'part_pkg_link',
- hashref => { 'src_pkgpart' => $self->pkgpart,
- 'link_type' => $type,
- #protection against infinite recursive links
- 'dst_pkgpart' => { op=>'!=', value=> $self->pkgpart },
- },
- order_by => "ORDER BY hidden",
- });
+
+ return @{ $cache_link{$type}->{$self->pkgpart} }
+ if $cache_enabled && $cache_link{$type}->{$self->pkgpart};
+
+ cluck $type.'_part_pkg_link called' if $DEBUG;
+
+ my @ppl =
+ qsearch({ table => 'part_pkg_link',
+ hashref => { src_pkgpart => $self->pkgpart,
+ link_type => $type,
+ #protection against infinite recursive links
+ dst_pkgpart => { op=>'!=', value=> $self->pkgpart },
+ },
+ order_by => "ORDER BY hidden",
+ });
+
+ $cache_link{$type}->{$self->pkgpart} = \@ppl if $cache_enabled;
+
+ return @ppl;
}
sub self_and_bill_linked {
$part_pkg_taxproduct ? $part_pkg_taxproduct->description : '';
}
+=item units_taxproduct
+
+Returns the L<FS::part_pkg_taxproduct> record used to report the taxable
+service units (usually phone lines) on this package.
+
+=cut
+
+sub units_taxproduct {
+ my $self = shift;
+ $self->units_taxproductnum
+ ? FS::part_pkg_taxproduct->by_key($self->units_taxproductnum)
+ : '';
+}
=item tax_rates DATA_PROVIDER, GEOCODE, [ CLASS ]
Returns the voice usage pools (see L<FS::part_pkg_usage>) defined for
this package.
+=item change_to_pkg
+
+Returns the automatic transfer target for this package, or an empty string
+if there isn't one.
+
+=cut
+
+sub change_to_pkg {
+ my $self = shift;
+ my $pkgpart = $self->change_to_pkgpart or return '';
+ FS::part_pkg->by_key($pkgpart);
+}
+
=item _rebless
Reblesses the object into the FS::part_pkg::PLAN class (if available), where
=item calc_setup CUST_PKG START_DATE DETAILS_ARRAYREF OPTIONS_HASHREF
-=item calc_recur CUST_PKG START_DATE DETAILS_ARRAYREF OPTIONS_HASHREF
+=item calc_recur CUST_PKG START_DATE_SCALARREF DETAILS_ARRAYREF OPTIONS_HASHREF
Calculates and returns the setup or recurring fees, respectively, for this
package. Implementation is in the FS::part_pkg:* module specific to this price
This option is filled in by the method rather than controlling its operation.
It is an arrayref. Applicable discounts will be added to the arrayref, as
-L<FS::cust_bill_pkg_discount|FS::cust_bill_pkg_discount records>.
+L<FS::cust_bill_pkg_discount> records.
=item real_pkgpart
-For package add-ons, is the base L<FS::part_pkg|package definition>, otherwise
+For package add-ons, is the base L<FS::part_pkg> package definition, otherwise
no different than pkgpart.
=item precommit_hooks
=back
Note: Don't calculate prices when not actually billing the package. For that,
-see the L</base_setup|base_setup> and L</base_recur|base_recur> methods.
+see the L<FS::cust_pkg/base_setup> and L<FS::cust_pkg/base_recur> methods.
=cut
=item calc_units CUST_PKG
This returns the number of provisioned svc_phone records, or, of the package
-count_available_phones option is set, the number available to be provisoined
+count_available_phones option is set, the number available to be provisioned
in the package.
=cut
-#fallback that returns 0 for old legacy packages with no plan
-sub calc_units { 0; }
+sub calc_units {
+ my($self, $cust_pkg ) = @_;
+ my $count = 0;
+ if ( $self->option('count_available_phones', 1)) {
+ foreach my $pkg_svc ($cust_pkg->part_pkg->pkg_svc) {
+ if ($pkg_svc->part_svc->svcdb eq 'svc_phone') { # svc_pbx?
+ $count += $pkg_svc->quantity || 0;
+ }
+ }
+ $count *= $cust_pkg->quantity;
+ } else {
+ $count =
+ scalar(grep { $_->part_svc->svcdb eq 'svc_phone' } $cust_pkg->cust_svc);
+ }
+ $count;
+}
#fallback for everything not based on flat.pm
sub recur_temporality { 'upcoming'; }
$self->base_recur_permonth(@_) - $self->recur_cost_permonth(@_);
}
+=item intro_end PACKAGE
+
+Takes an L<FS::cust_pkg> object. If this plan has an introductory rate,
+returns the expected date the intro period will end. If there is no intro
+rate, returns zero.
+
+=cut
+
+sub intro_end {
+ 0;
+}
+
=item format OPTION DATA
Returns data formatted according to the function 'format' described
FS::upgrade_journal->set_done($upgrade);
}
+ # migrate adjourn_months, expire_months, and contract_end_months to
+ # real fields
+ foreach my $field (qw(adjourn_months expire_months contract_end_months)) {
+ foreach my $option (qsearch('part_pkg_option', { optionname => $field })) {
+ my $part_pkg = $option->part_pkg;
+ my $error = $option->delete;
+ if ( $option->optionvalue and $part_pkg->get($field) eq '' ) {
+ $part_pkg->set($field, $option->optionvalue);
+ $error ||= $part_pkg->replace;
+ }
+ die $error if $error;
+ }
+ }
+
+ # remove custom flag from one-time charge packages that were accidentally
+ # flagged as custom
+ $search = FS::Cursor->new({
+ 'table' => 'part_pkg',
+ 'hashref' => { 'freq' => '0',
+ 'custom' => 'Y',
+ 'family_pkgpart' => { op => '!=', value => '' },
+ },
+ 'addl_from' => ' JOIN
+ (select pkgpart from cust_pkg group by pkgpart having count(*) = 1)
+ AS singular_pkg USING (pkgpart)',
+ });
+ my @fields = grep { $_ ne 'pkgpart'
+ and $_ ne 'custom'
+ and $_ ne 'disabled' } FS::part_pkg->fields;
+ PKGPART: while (my $part_pkg = $search->fetch) {
+ # can't merge the package back into its parent (too late for that)
+ # but we can remove the custom flag if it's not actually customized,
+ # i.e. nothing has been changed.
+
+ my $family_pkgpart = $part_pkg->family_pkgpart;
+ next PKGPART if $family_pkgpart == $part_pkg->pkgpart;
+ my $parent_pkg = FS::part_pkg->by_key($family_pkgpart);
+ foreach my $field (@fields) {
+ if ($part_pkg->get($field) ne $parent_pkg->get($field)) {
+ next PKGPART;
+ }
+ }
+ # options have to be identical too
+ # but links, FCC options, discount plans, and usage packages can't be
+ # changed through the "modify charge" UI, so skip them
+ my %newopt = $part_pkg->options;
+ my %oldopt = $parent_pkg->options;
+ OPTION: foreach my $option (keys %newopt) {
+ if (delete $newopt{$option} ne delete $oldopt{$option}) {
+ next PKGPART;
+ }
+ }
+ if (keys(%newopt) or keys(%oldopt)) {
+ next PKGPART;
+ }
+ # okay, now replace it
+ warn "Removing custom flag from part_pkg#".$part_pkg->pkgpart."\n";
+ $part_pkg->set('custom', '');
+ my $error = $part_pkg->replace;
+ die $error if $error;
+ } # $search->fetch
+
+ return;
}
=item curuser_pkgs_sql
}
+=item join_options_sql
+
+Returns an SQL fragment for JOINing the part_pkg_option records for this
+package's setup_fee and recur_fee (as setup_option and recur_option,
+respectively). Useful for optimization.
+
+=cut
+
+sub join_options_sql {
+ #my $class = shift;
+ "
+ LEFT JOIN part_pkg_option AS setup_option
+ ON ( part_pkg.pkgpart = setup_option.pkgpart
+ AND setup_option.optionname = 'setup_fee' )
+ LEFT JOIN part_pkg_option AS recur_option
+ ON ( part_pkg.pkgpart = recur_option.pkgpart
+ AND recur_option.optionname = 'recur_fee' )
+ ";
+}
+
=back
=head1 SUBROUTINES
=cut
1;
-