+sub calc_units { 0; }
+
+#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
+in the plan info. Returns DATA if no such function exists.
+
+=cut
+
+sub format {
+ my ($self, $option, $data) = (shift, shift, shift);
+ if (exists($plans{$self->plan}->{fields}->{$option}{format})) {
+ &{$plans{$self->plan}->{fields}->{$option}{format}}($data);
+ }else{
+ $data;
+ }
+}
+
+=item parse OPTION DATA
+
+Returns data parsed according to the function 'parse' described
+in the plan info. Returns DATA if no such function exists.
+
+=cut
+
+sub parse {
+ my ($self, $option, $data) = (shift, shift, shift);
+ if (exists($plans{$self->plan}->{fields}->{$option}{parse})) {
+ &{$plans{$self->plan}->{fields}->{$option}{parse}}($data);
+ }else{
+ $data;
+ }
+}
+
+=back
+
+=cut
+
+=head1 CLASS METHODS
+
+=over 4
+
+=cut
+
+# _upgrade_data
+#
+# Used by FS::Upgrade to migrate to a new database.
+
+sub _upgrade_data { # class method
+ my($class, %opts) = @_;
+
+ warn "[FS::part_pkg] upgrading $class\n" if $DEBUG;
+
+ my @part_pkg = qsearch({
+ 'table' => 'part_pkg',
+ 'extra_sql' => "WHERE ". join(' OR ',
+ ( map "($_ IS NOT NULL AND $_ != '' )",
+ qw( plandata setup recur ) ),
+ 'plan IS NULL', "plan = '' ",
+ ),
+ });
+
+ foreach my $part_pkg (@part_pkg) {
+
+ unless ( $part_pkg->plan ) {
+ $part_pkg->plan('flat');
+ }
+
+ if ( length($part_pkg->option('setup_fee')) == 0
+ && $part_pkg->setup =~ /^\s*([\d\.]+)\s*$/ ) {
+
+ my $opt = new FS::part_pkg_option {
+ 'pkgpart' => $part_pkg->pkgpart,
+ 'optionname' => 'setup_fee',
+ 'optionvalue' => $1,
+ };
+ my $error = $opt->insert;
+ die $error if $error;
+
+
+ #} else {
+ # die "Can't parse part_pkg.setup for fee; convert pkgnum ".
+ # $part_pkg->pkgnum. " manually: ". $part_pkg->setup. "\n";
+ }
+ $part_pkg->setup('');
+
+ if ( length($part_pkg->option('recur_fee')) == 0
+ && $part_pkg->recur =~ /^\s*([\d\.]+)\s*$/ ) {
+
+ my $opt = new FS::part_pkg_option {
+ 'pkgpart' => $part_pkg->pkgpart,
+ 'optionname' => 'recur_fee',
+ 'optionvalue' => $1,
+ };
+ my $error = $opt->insert;
+ die $error if $error;
+
+
+ #} else {
+ # die "Can't parse part_pkg.setup for fee; convert pkgnum ".
+ # $part_pkg->pkgnum. " manually: ". $part_pkg->setup. "\n";
+ }
+ $part_pkg->recur('');
+
+ $part_pkg->replace; #this should take care of plandata, right?
+
+ }
+
+ # now upgrade to the explicit custom flag
+
+ @part_pkg = qsearch({
+ 'table' => 'part_pkg',
+ 'hashref' => { disabled => 'Y', custom => '' },
+ 'extra_sql' => "AND comment LIKE '(CUSTOM) %'",
+ });
+
+ foreach my $part_pkg (@part_pkg) {
+ my $new = new FS::part_pkg { $part_pkg->hash };
+ $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 };
+ my $primary = $part_pkg->svcpart;
+ my $options = { $part_pkg->options };
+
+ my $error = $new->replace( $part_pkg,
+ 'pkg_svc' => $pkg_svc,
+ 'primary_svc' => $primary,
+ 'options' => $options,
+ );
+ 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;
+ }
+ }
+
+ # migrate use_disposition_taqua and use_disposition to disposition_in
+ @part_pkg_option = qsearch('part_pkg_option',
+ { 'optionname' => { op => 'LIKE',
+ value => 'use_disposition%',
+ },
+ 'optionvalue' => 1,
+ });
+ my %newopts = map { $_->pkgpart => $_ }
+ qsearch('part_pkg_option', { 'optionname' => 'disposition_in', } );
+ foreach my $old_opt (@part_pkg_option) {
+ my $pkgpart = $old_opt->pkgpart;
+ my $newval = $old_opt->optionname eq 'use_disposition_taqua' ? '100'
+ : 'ANSWERED';
+ my $error = $old_opt->delete;
+ die $error if $error;
+
+ if ( exists($newopts{$pkgpart}) ) {
+ my $opt = $newopts{$pkgpart};
+ $opt->optionvalue($opt->optionvalue.",$newval");
+ $error = $opt->replace;
+ die $error if $error;
+ } else {
+ my $new_opt = new FS::part_pkg_option {
+ 'pkgpart' => $pkgpart,
+ 'optionname' => 'disposition_in',
+ 'optionvalue' => $newval,
+ };
+ $error = $new_opt->insert;
+ die $error if $error;
+ $newopts{$pkgpart} = $new_opt;
+ }
+ }
+
+}
+
+=item curuser_pkgs_sql
+
+Returns an SQL fragment for searching for packages the current user can
+use, either via part_pkg.agentnum directly, or via agent type (see
+L<FS::type_pkgs>).
+
+=cut
+
+sub curuser_pkgs_sql {
+ my $class = shift;
+
+ $class->_pkgs_sql( $FS::CurrentUser::CurrentUser->agentnums );
+
+}
+
+=item agent_pkgs_sql AGENT | AGENTNUM, ...
+
+Returns an SQL fragment for searching for packages the provided agent or agents
+can use, either via part_pkg.agentnum directly, or via agent type (see
+L<FS::type_pkgs>).
+
+=cut
+
+sub agent_pkgs_sql {
+ my $class = shift; #i'm a class method, not a sub (the question is... why??)
+ my @agentnums = map { ref($_) ? $_->agentnum : $_ } @_;
+
+ $class->_pkgs_sql(@agentnums); #is this why
+
+}
+
+sub _pkgs_sql {
+ my( $class, @agentnums ) = @_;
+ my $agentnums = join(',', @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)
+ )
+ )
+ )
+ ";
+
+}