package FS::part_pkg;
use strict;
-use vars qw( @ISA %plans $DEBUG $setup_hack );
+use vars qw( @ISA %plans $DEBUG $setup_hack $skip_pkg_svc_hack );
use Carp qw(carp cluck confess);
use Scalar::Util qw( blessed );
use Time::Local qw( timelocal_nocheck );
use FS::part_pkg_option;
use FS::pkg_class;
use FS::agent;
+use FS::part_pkg_taxrate;
use FS::part_pkg_taxoverride;
use FS::part_pkg_taxproduct;
use FS::part_pkg_link;
+use FS::part_pkg_discount;
@ISA = qw( FS::m2m_Common FS::option_Common );
$DEBUG = 0;
$setup_hack = 0;
+$skip_pkg_svc_hack = 0;
=head1 NAME
=item agentnum - Optional agentnum (see L<FS::agent>)
+=item fcc_ds0s - Optional DS0 equivalency number for FCC form 477
+
=back
=head1 METHODS
I<custnum_ref> and I<options>.
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.
+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.
If I<primary_svc> is set to the svcpart of the primary service, the appropriate
FS::pkg_svc record will be updated.
warn " inserting part_pkg_taxoverride records" if $DEBUG;
my %overrides = %{ $options{'tax_overrides'} || {} };
foreach my $usage_class ( keys %overrides ) {
- my @overrides = (grep "$_", split (',', $overrides{$usage_class}) );
+ my $override =
+ ( exists($overrides{$usage_class}) && defined($overrides{$usage_class}) )
+ ? $overrides{$usage_class}
+ : '';
+ my @overrides = (grep "$_", split(',', $override) );
my $error = $self->process_m2m (
'link_table' => 'part_pkg_taxoverride',
'target_table' => 'tax_class',
}
}
- warn " inserting pkg_svc records" if $DEBUG;
- my $pkg_svc = $options{'pkg_svc'} || {};
- foreach my $part_svc ( qsearch('part_svc', {} ) ) {
- my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
- my $primary_svc =
- ( $options{'primary_svc'} && $options{'primary_svc'}==$part_svc->svcpart )
- ? 'Y'
- : '';
-
- my $pkg_svc = new FS::pkg_svc( {
- 'pkgpart' => $self->pkgpart,
- 'svcpart' => $part_svc->svcpart,
- 'quantity' => $quantity,
- 'primary_svc' => $primary_svc,
- } );
- my $error = $pkg_svc->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
+ unless ( $skip_pkg_svc_hack ) {
+
+ warn " inserting pkg_svc records" if $DEBUG;
+ my $pkg_svc = $options{'pkg_svc'} || {};
+ my $hidden_svc = $options{'hidden_svc'} || {};
+ foreach my $part_svc ( qsearch('part_svc', {} ) ) {
+ my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
+ my $primary_svc =
+ ( $options{'primary_svc'} && $options{'primary_svc'}==$part_svc->svcpart )
+ ? 'Y'
+ : '';
+
+ my $pkg_svc = new FS::pkg_svc( {
+ 'pkgpart' => $self->pkgpart,
+ 'svcpart' => $part_svc->svcpart,
+ 'quantity' => $quantity,
+ 'primary_svc' => $primary_svc,
+ 'hidden' => $hidden_svc->{$part_svc->svcpart},
+ } );
+ my $error = $pkg_svc->insert;
+ 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<primary_svc> and I<options>
+Currently available options are: I<pkg_svc>, I<hidden_svc>, I<primary_svc>
+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.
+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.
If I<primary_svc> is set to the svcpart of the primary service, the appropriate
FS::pkg_svc record will be updated.
warn " replacing pkg_svc records" if $DEBUG;
my $pkg_svc = $options->{'pkg_svc'} || {};
+ my $hidden_svc = $options->{'hidden_svc'} || {};
foreach my $part_svc ( qsearch('part_svc', {} ) ) {
my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
+ my $hidden = $hidden_svc->{$part_svc->svcpart} || '';
my $primary_svc =
( defined($options->{'primary_svc'}) && $options->{'primary_svc'}
&& $options->{'primary_svc'} == $part_svc->svcpart
? 'Y'
: '';
-
my $old_pkg_svc = qsearchs('pkg_svc', {
- 'pkgpart' => $old->pkgpart,
- 'svcpart' => $part_svc->svcpart,
- } );
- my $old_quantity = $old_pkg_svc ? $old_pkg_svc->quantity : 0;
- my $old_primary_svc =
- ( $old_pkg_svc && $old_pkg_svc->dbdef_table->column('primary_svc') )
- ? $old_pkg_svc->primary_svc
- : '';
- next unless $old_quantity != $quantity || $old_primary_svc ne $primary_svc;
+ 'pkgpart' => $old->pkgpart,
+ 'svcpart' => $part_svc->svcpart,
+ }
+ );
+ my $old_quantity = 0;
+ my $old_primary_svc = '';
+ my $old_hidden = '';
+ 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;
+ }
+
+ next unless $old_quantity != $quantity ||
+ $old_primary_svc ne $primary_svc ||
+ $old_hidden ne $hidden;
my $new_pkg_svc = new FS::pkg_svc( {
'pkgsvcnum' => ( $old_pkg_svc ? $old_pkg_svc->pkgsvcnum : '' ),
'svcpart' => $part_svc->svcpart,
'quantity' => $quantity,
'primary_svc' => $primary_svc,
+ 'hidden' => $hidden,
} );
my $error = $old_pkg_svc
? $new_pkg_svc->replace($old_pkg_svc)
|| $self->ut_textn('taxclass')
|| $self->ut_enum('disabled', [ '', 'Y' ] )
|| $self->ut_enum('custom', [ '', 'Y' ] )
+ || $self->ut_enum('no_auto', [ '', 'Y' ])
#|| $self->ut_moneyn('setup_cost')
#|| $self->ut_moneyn('recur_cost')
|| $self->ut_floatn('setup_cost')
|| $self->ut_floatn('pay_weight')
|| $self->ut_floatn('credit_weight')
|| $self->ut_numbern('taxproductnum')
+ || $self->ut_foreign_keyn('classnum', 'pkg_class', 'classnum')
+ || $self->ut_foreign_keyn('addon_classnum', 'pkg_class', 'classnum')
|| $self->ut_foreign_keyn('taxproductnum',
'part_pkg_taxproduct',
'taxproductnum'
? $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum' )
: $self->ut_agentnum_acl('agentnum', \@null_agentnum_right)
)
+ || $self->ut_numbern('fcc_ds0s')
|| $self->SUPER::check
;
return $error if $error;
- if ( $self->classnum !~ /^$/ ) {
- my $error = $self->ut_foreign_key('classnum', 'pkg_class', 'classnum');
- return $error if $error;
- } else {
- $self->classnum('');
- }
-
return 'Unknown plan '. $self->plan
unless exists($plans{$self->plan});
$pre. $self->pkg. ' - '. $self->custom_comment;
}
+sub price_info { # safety, in case a part_pkg hasn't defined price_info
+ '';
+}
+
sub custom_comment {
my $self = shift;
- ( $self->custom ? '(CUSTOM) ' : '' ). $self->comment;
+ ( $self->custom ? '(CUSTOM) ' : '' ). $self->comment . ' ' . $self->price_info;
}
=item pkg_class
}
}
+=item addon_pkg_class
+
+Returns the add-on package class, as an FS::pkg_class object, or the empty
+string if there is no add-on package class.
+
+=cut
+
+sub addon_pkg_class {
+ my $self = shift;
+ if ( $self->addon_classnum ) {
+ qsearchs('pkg_class', { 'classnum' => $self->addon_classnum } );
+ } else {
+ return '';
+ }
+}
+
=item categoryname
Returns the package category name, or the empty string if there is no package
: '';
}
+=item addon_classname
+
+Returns the add-on package class name, or the empty string if there is no
+add-on package class.
+
+=cut
+
+sub addon_classname {
+ my $self = shift;
+ my $pkg_class = $self->addon_pkg_class;
+ $pkg_class
+ ? $pkg_class->classname
+ : '';
+}
+
=item agent
Returns the associated agent for this event, if any, as an FS::agent object.
}
}
+sub can_discount { 0; }
sub freqs_href {
- #method, class method or sub? #my $self = shift;
-
- tie my %freq, 'Tie::IxHash',
- '0' => '(no recurring fee)',
- '1h' => 'hourly',
- '1d' => 'daily',
- '2d' => 'every two days',
- '3d' => 'every three days',
- '1w' => 'weekly',
- '2w' => 'biweekly (every 2 weeks)',
- '1' => 'monthly',
- '45d' => 'every 45 days',
- '2' => 'bimonthly (every 2 months)',
- '3' => 'quarterly (every 3 months)',
- '4' => 'every 4 months',
- '137d' => 'every 4 1/2 months (137 days)',
- '6' => 'semiannually (every 6 months)',
- '12' => 'annually',
- '13' => 'every 13 months (annually +1 month)',
- '24' => 'biannually (every 2 years)',
- '36' => 'triannually (every 3 years)',
- '48' => '(every 4 years)',
- '60' => '(every 5 years)',
- '120' => '(every 10 years)',
- ;
-
- \%freq;
-
+ # moved to FS::Misc to make this accessible to other packages
+ # at initialization
+ FS::Misc::pkg_freqs();
}
=item freq_pretty
}
}
-=item add_freq TIMESTAMP
+=item add_freq TIMESTAMP [ FREQ ]
-Adds the frequency of this package to the provided timestamp and returns
-the resulting timestamp, or -1 if the frequency of this package could not be
-parsed (shouldn't happen).
+Adds a billing period of some frequency to the provided timestamp and
+returns the resulting timestamp, or -1 if the frequency could not be
+parsed (shouldn't happen). By default, the frequency of this package
+will be used; to override this, pass a different frequency as a second
+argument.
=cut
sub add_freq {
- my( $self, $date ) = @_;
- my $freq = $self->freq;
+ my( $self, $date, $freq ) = @_;
+ $freq = $self->freq unless $freq;
#change this bit to use Date::Manip? CAREFUL with timezones (see
# mailing list archive)
my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($date) )[0,1,2,3,4,5];
- if ( $self->freq =~ /^\d+$/ ) {
- $mon += $self->freq;
+ if ( $freq =~ /^\d+$/ ) {
+ $mon += $freq;
until ( $mon < 12 ) { $mon -= 12; $year++; }
- } elsif ( $self->freq =~ /^(\d+)w$/ ) {
+ } elsif ( $freq =~ /^(\d+)w$/ ) {
my $weeks = $1;
$mday += $weeks * 7;
- } elsif ( $self->freq =~ /^(\d+)d$/ ) {
+ } elsif ( $freq =~ /^(\d+)d$/ ) {
my $days = $1;
$mday += $days;
- } elsif ( $self->freq =~ /^(\d+)h$/ ) {
+ } elsif ( $freq =~ /^(\d+)h$/ ) {
my $hours = $1;
$hour += $hours;
} else {
map { $_->optionname => $_->optionvalue } $self->part_pkg_option;
}
-=item option OPTIONNAME
+=item option OPTIONNAME [ QUIET ]
-Returns the option value for the given name, or the empty string.
+Returns the option value for the given name, or the empty string. If a true
+value is passed as the second argument, warnings about missing the option
+will be suppressed.
=cut
sub _part_pkg_link {
my( $self, $type ) = @_;
- qsearch('part_pkg_link', { 'src_pkgpart' => $self->pkgpart,
- 'link_type' => $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",
+ });
}
sub self_and_bill_linked {
}
sub _self_and_linked {
- my( $self, $type ) = @_;
+ my( $self, $type, $hidden ) = @_;
+ $hidden ||= '';
+
+ my @result = ();
+ foreach ( ( $self, map { $_->dst_pkg->_self_and_linked($type, $_->hidden) }
+ $self->_part_pkg_link($type) ) )
+ {
+ $_->hidden($hidden) if $hidden;
+ push @result, $_;
+ }
- ( $self,
- map { $_->dst_pkg->_self_and_linked($type) }
- $self->_part_pkg_link($type)
- );
+ (@result);
}
=item part_pkg_taxoverride [ CLASS ]
} );
}
+=item part_pkg_discount
+
+Returns the package to discount m2m records (see L<FS::part_pkg_discount>)
+for this package.
+
+=cut
+
+sub part_pkg_discount {
+ my $self = shift;
+ qsearch('part_pkg_discount', { 'pkgpart' => $self->pkgpart });
+}
+
=item _rebless
Reblesses the object into the FS::part_pkg::PLAN class (if available), where
#fallback for everything except bulk.pm
sub hide_svc_detail { 0; }
+=item recur_cost_permonth CUST_PKG
+
+recur_cost divided by freq (only supported for monthly and longer frequencies)
+
+=cut
+
+sub recur_cost_permonth {
+ my($self, $cust_pkg) = @_;
+ return 0 unless $self->freq =~ /^\d+$/ && $self->freq > 0;
+ sprintf('%.2f', $self->recur_cost / $self->freq );
+}
+
=item format OPTION DATA
Returns data formatted according to the function 'format' described
$new->custom('Y');
my $comment = $part_pkg->comment;
$comment =~ s/^\(CUSTOM\) //;
+ $comment = '(none)' unless $comment =~ /\S/;
$new->comment($comment);
my $pkg_svc = { map { $_->svcpart => $_->quantity } $part_pkg->pkg_svc };
die $error if $error;
}
+ my @part_pkg_option = qsearch('part_pkg_option',
+ { 'optionname' => 'unused_credit',
+ 'optionvalue' => 1,
+ });
+ foreach my $old_opt (@part_pkg_option) {
+ my $pkgpart = $old_opt->pkgpart;
+ my $error = $old_opt->delete;
+ die $error if $error;
+
+ foreach (qw(unused_credit_cancel unused_credit_change)) {
+ my $new_opt = new FS::part_pkg_option {
+ 'pkgpart' => $pkgpart,
+ 'optionname' => $_,
+ 'optionvalue' => 1,
+ };
+ $error = $new_opt->insert;
+ die $error if $error;
+ }
+ }
}
=item curuser_pkgs_sql
"
(
- agentnum IS NOT NULL
- OR
- 0 < ( SELECT COUNT(*)
- FROM type_pkgs
- LEFT JOIN agent_type USING ( typenum )
- LEFT JOIN agent AS typeagent USING ( typenum )
- WHERE type_pkgs.pkgpart = part_pkg.pkgpart
- AND typeagent.agentnum IN ($agentnums)
- )
+ ( agentnum IS NOT NULL AND agentnum IN ($agentnums) )
+ OR ( agentnum IS NULL
+ AND EXISTS ( SELECT 1
+ FROM type_pkgs
+ LEFT JOIN agent_type USING ( typenum )
+ LEFT JOIN agent AS typeagent USING ( typenum )
+ WHERE type_pkgs.pkgpart = part_pkg.pkgpart
+ AND typeagent.agentnum IN ($agentnums)
+ )
+ )
)
";
next;
}
warn "got plan info from FS::part_pkg::$mod: $info\n" if $DEBUG;
- if ( exists($info->{'disabled'}) && $info->{'disabled'} ) {
- warn "skipping disabled plan FS::part_pkg::$mod" if $DEBUG;
- next;
- }
+ #if ( exists($info->{'disabled'}) && $info->{'disabled'} ) {
+ # warn "skipping disabled plan FS::part_pkg::$mod" if $DEBUG;
+ # next;
+ #}
$info{$mod} = $info;
+ $info->{'weight'} ||= 0; # quiet warnings
}
}
+# copy one level deep to allow replacement of fields and fieldorder
tie %plans, 'Tie::IxHash',
- map { $_ => $info{$_} }
+ map { my %infohash = %{ $info{$_} };
+ $_ => \%infohash }
sort { $info{$a}->{'weight'} <=> $info{$b}->{'weight'} }
keys %info;
+# inheritance of plan options
+foreach my $name (keys(%info)) {
+ if (exists($info{$name}->{'disabled'}) and $info{$name}->{'disabled'}) {
+ warn "skipping disabled plan FS::part_pkg::$name" if $DEBUG;
+ delete $plans{$name};
+ next;
+ }
+ my $parents = $info{$name}->{'inherit_fields'} || [];
+ my (%fields, %field_exists, @fieldorder);
+ foreach my $parent ($name, @$parents) {
+ %fields = ( # avoid replacing existing fields
+ %{ $info{$parent}->{'fields'} || {} },
+ %fields
+ );
+ foreach (@{ $info{$parent}->{'fieldorder'} || [] }) {
+ # avoid duplicates
+ next if $field_exists{$_};
+ $field_exists{$_} = 1;
+ # allow inheritors to remove inherited fields from the fieldorder
+ push @fieldorder, $_ if !exists($fields{$_}->{'disabled'});
+ }
+ }
+ $plans{$name}->{'fields'} = \%fields;
+ $plans{$name}->{'fieldorder'} = \@fieldorder;
+}
+
sub plan_info {
\%plans;
}