package FS::cust_pkg;
-use base qw( FS::cust_pkg::Search
+use base qw( FS::cust_pkg::Search FS::cust_pkg::API
FS::otaker_Mixin FS::cust_main_Mixin FS::Sales_Mixin
FS::contact_Mixin FS::location_Mixin
FS::m2m_Common FS::option_Common
$self->order_date(time) unless ($import && $self->order_date)
or $self->change_pkgnum;
+ $self->susp( $self->order_date ) if $self->susp eq 'now';
+
my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
|| $self->ut_numbern('dundate')
|| $self->ut_enum('no_auto', [ '', 'Y' ])
|| $self->ut_enum('waive_setup', [ '', 'Y' ])
- || $self->ut_numbern('agent_pkgid')
+ || $self->ut_textn('agent_pkgid')
|| $self->ut_enum('recur_show_zero', [ '', 'Y', 'N', ])
|| $self->ut_enum('setup_show_zero', [ '', 'Y', 'N', ])
|| $self->ut_foreign_keyn('main_pkgnum', 'cust_pkg', 'pkgnum')
'to' => \@invoicing_list,
'subject' => ( $conf->config('cancelsubject') || 'Cancellation Notice' ),
'body' => [ map "$_\n", $conf->config('cancelmessage') ],
+ 'custnum' => $self->custnum,
+ 'msgtype' => '', #admin?
);
}
#should this do something on errors?
'Package : #'. $self->pkgnum. " (". $self->part_pkg->pkg_comment. ")\n",
( map { "Service : $_\n" } @labels ),
],
+ 'custnum' => $self->custnum,
+ 'msgtype' => 'admin'
);
if ( $error ) {
my $conf = new FS::Conf;
- if ( $inactive > 0 &&
- ( $hash{'bill'} || $hash{'setup'} ) &&
- ( $opt{'adjust_next_bill'} ||
- $conf->exists('unsuspend-always_adjust_next_bill_date') ||
- $self->part_pkg->option('unsuspend_adjust_bill', 1) )
- ) {
-
- $hash{'bill'} = ( $hash{'bill'} || $hash{'setup'} ) + $inactive;
-
- }
+ #adjust the next bill date forward
+ $hash{'bill'} = ( $hash{'bill'} || $hash{'setup'} ) + $inactive
+ if $inactive > 0
+ && ( $hash{'bill'} || $hash{'setup'} )
+ && ( $opt{'adjust_next_bill'}
+ || $conf->exists('unsuspend-always_adjust_next_bill_date')
+ || $self->part_pkg->option('unsuspend_adjust_bill', 1)
+ )
+ && ! $self->option('suspend_bill',1)
+ && ( ! $self->part_pkg->option('suspend_bill',1)
+ || $self->option('no_suspend_bill',1)
+ )
+ && $hash{'order_date'} != $hash{'susp'}
+ ;
$hash{'susp'} = '';
$hash{'adjourn'} = '' if $hash{'adjourn'} and $hash{'adjourn'} < time;
: ''
),
],
+ 'custnum' => $self->custnum,
+ 'msgtype' => 'admin',
);
if ( $error ) {
}
my %pkg_opt = $part_pkg->options;
- if ( ref($opt{'additional'}) ) {
- delete $pkg_opt{$_} foreach grep /^additional/, keys %pkg_opt;
- my $i;
- for ( $i = 0; exists($opt{'additional'}->[$i]); $i++ ) {
- $pkg_opt{ "additional_info$i" } = $opt{'additional'}->[$i];
+ my $pkg_opt_modified = 0;
+
+ $opt{'additional'} ||= [];
+ my $i;
+ my @old_additional;
+ foreach (grep /^additional/, keys %pkg_opt) {
+ ($i) = ($_ =~ /^additional_info(\d+)$/);
+ $old_additional[$i] = $pkg_opt{$_} if $i;
+ delete $pkg_opt{$_};
+ }
+
+ for ( $i = 0; exists($opt{'additional'}->[$i]); $i++ ) {
+ $pkg_opt{ "additional_info$i" } = $opt{'additional'}->[$i];
+ if (!exists($old_additional[$i])
+ or $old_additional[$i] ne $opt{'additional'}->[$i])
+ {
+ $pkg_opt_modified = 1;
}
- $pkg_opt{'additional_count'} = $i if $i > 0;
}
+ $pkg_opt_modified = 1 if (scalar(@old_additional) - 1) != $i;
+ $pkg_opt{'additional_count'} = $i if $i > 0;
my $old_classnum;
if ( exists($opt{'classnum'}) and $part_pkg->classnum ne $opt{'classnum'} )
$self->set('start_date', $opt{'start_date'});
}
- if ($self->modified) { # for quantity or start_date change
- my $error = $self->replace;
- return $error if $error;
- }
if ( exists($opt{'amount'})
and $part_pkg->option('setup_fee') != $opt{'amount'}
and $opt{'amount'} > 0 ) {
$pkg_opt{'setup_fee'} = $opt{'amount'};
- # standard for one-time charges is to set comment = (formatted) amount
- # update it to avoid confusion
- my $conf = FS::Conf->new;
- $part_pkg->set('comment',
- ($conf->config('money_char') || '$') .
- sprintf('%.2f', $opt{'amount'})
- );
+ $pkg_opt_modified = 1;
+
}
} # else simply ignore them; the UI shouldn't allow editing the fields
- my $error = $part_pkg->replace( options => \%pkg_opt );
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
+ my $error;
+ if ( $part_pkg->modified or $pkg_opt_modified ) {
+ # can we safely modify the package def?
+ # Yes, if it's not available for purchase, and this is the only instance
+ # of it.
+ if ( $part_pkg->disabled
+ and FS::cust_pkg->count('pkgpart = '.$part_pkg->pkgpart) == 1
+ and FS::quotation_pkg->count('pkgpart = '.$part_pkg->pkgpart) == 0
+ ) {
+ $error = $part_pkg->replace( options => \%pkg_opt );
+ } else {
+ # clone it
+ $part_pkg = $part_pkg->clone;
+ $part_pkg->set('disabled' => 'Y');
+ $error = $part_pkg->insert( options => \%pkg_opt );
+ # and associate this as yet-unbilled package to the new package def
+ $self->set('pkgpart' => $part_pkg->pkgpart);
+ }
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
}
+ if ($self->modified) { # for quantity or start_date change, or if we had
+ # to clone the existing package def
+ my $error = $self->replace;
+ return $error if $error;
+ }
if (defined $old_classnum) {
# fix invoice grouping records
my $old_catname = $old_classnum
=item cust_event
-Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
+Returns the customer billing events (see L<FS::cust_event>) for this invoice.
=cut
=item num_cust_event
-Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
+Returns the number of customer billing events (see L<FS::cust_event>) for this package.
=cut
#false laziness w/cust_bill.pm
sub num_cust_event {
my $self = shift;
- my $sql =
- "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
- " WHERE tablenum = ? AND eventtable = 'cust_pkg'";
+ my $sql = "SELECT COUNT(*) ". $self->_from_cust_event_where;
+ $self->_prep_ex($sql, $self->pkgnum)->fetchrow_arrayref->[0];
+}
+
+=item exists_cust_event
+
+Returns true if there are customer billing events (see L<FS::cust_event>) for this package. More efficient than using num_cust_event.
+
+=cut
+
+sub exists_cust_event {
+ my $self = shift;
+ my $sql = "SELECT 1 ". $self->_from_cust_event_where. " LIMIT 1";
+ my $row = $self->_prep_ex($sql, $self->pkgnum)->fetchrow_arrayref;
+ $row ? $row->[0] : '';
+}
+
+sub _from_cust_event_where {
+ #my $self = shift;
+ " FROM cust_event JOIN part_event USING ( eventpart ) ".
+ " WHERE tablenum = ? AND eventtable = 'cust_pkg' ";
+}
+
+sub _prep_ex {
+ my( $self, $sql, @args ) = @_;
my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
- $sth->execute($self->pkgnum) or die $sth->errstr. " executing $sql";
- $sth->fetchrow_arrayref->[0];
+ $sth->execute(@args) or die $sth->errstr. " executing $sql";
+ $sth;
}
=item part_pkg_currency_option OPTIONNAME
sub cust_svc_unsorted_arrayref {
my $self = shift;
- return () unless $self->num_cust_svc(@_);
+ return [] unless $self->num_cust_svc(@_);
my %opt = ();
if ( @_ && $_[0] =~ /^\d+/ ) {
=over 4
-=item num_cust_svc (count)
+=item num_cust_svc
+
+(count)
+
+=item num_avail
+
+(quantity - count)
-=item num_avail (quantity - count)
+=item cust_pkg_svc
-=item cust_pkg_svc (services) - array reference containing the provisioned services, as cust_svc objects
+(services) - array reference containing the provisioned services, as cust_svc objects
=back
-Accepts one option: summarize_size. If specified and non-zero, will omit the
-extra cust_pkg_svc option for objects where num_cust_svc is this size or
-greater.
+Accepts two options:
+
+=over 4
+
+=item summarize_size
+
+If true, will omit the extra cust_pkg_svc option for objects where num_cust_svc
+is this size or greater.
+
+=item hide_discontinued
+
+If true, will omit looking for services that are no longer avaialble in the
+package definition.
+
+=back
=cut
$part_svc;
} $self->part_pkg->pkg_svc;
- #extras
- push @part_svc, map {
- my $part_svc = $_;
- my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
- $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #speak no evail
- $part_svc->{'Hash'}{'num_avail'} = 0; #0-$num_cust_svc ?
- $part_svc->{'Hash'}{'cust_pkg_svc'} =
- $num_cust_svc ? [ $self->cust_svc($part_svc->svcpart) ] : [];
- $part_svc;
- } $self->extra_part_svc;
+ unless ( $opt{hide_discontinued} ) {
+ #extras
+ push @part_svc, map {
+ my $part_svc = $_;
+ my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
+ $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #speak no evail
+ $part_svc->{'Hash'}{'num_avail'} = 0; #0-$num_cust_svc ?
+ $part_svc->{'Hash'}{'cust_pkg_svc'} =
+ $num_cust_svc ? [ $self->cust_svc($part_svc->svcpart) ] : [];
+ $part_svc;
+ } $self->extra_part_svc;
+ }
@part_svc;
=over 4
+=item on hold
+
=item not yet billed
=item one-time charge
my $freq = length($self->freq) ? $self->freq : $self->part_pkg->freq;
return 'cancelled' if $self->get('cancel');
+ return 'on hold' if $self->susp && ! $self->setup;
return 'suspended' if $self->susp;
return 'not yet billed' unless $self->setup;
return 'one-time charge' if $freq =~ /^(0|$)/;
=cut
tie my %statuscolor, 'Tie::IxHash',
+ 'on hold' => '7E0079', #purple!
'not yet billed' => '009999', #teal? cyan?
- 'one-time charge' => '000000',
+ 'one-time charge' => '0000CC', #blue #'000000',
'active' => '00CC00',
'suspended' => 'FF9900',
'cancelled' => 'FF0000',
keys %statuscolor;
}
+sub statuscolors {
+ #my $self = shift;
+ \%statuscolor;
+}
+
=item statuscolor
Returns a hex triplet color string for this package's status.
sub tax_location {
my $self = shift;
- FS::cust_location->by_key( $self->tax_locationnum )
+ my $conf = FS::Conf->new;
+ if ( $conf->exists('tax-pkg_address') and $self->locationnum ) {
+ return FS::cust_location->by_key($self->locationnum);
+ }
+ elsif ( $conf->exists('tax-ship_address') ) {
+ return $self->cust_main->ship_location;
+ }
+ else {
+ return $self->cust_main->bill_location;
+ }
}
=item seconds_since TIMESTAMP
minutes => min($cust_pkg_usage->minutes, $minutes),
});
$cust_pkg_usage->set('minutes',
- sprintf('%.0f', $cust_pkg_usage->minutes - $cdr_cust_pkg_usage->minutes)
+ $cust_pkg_usage->minutes - $cdr_cust_pkg_usage->minutes
);
$error = $cust_pkg_usage->replace || $cdr_cust_pkg_usage->insert;
$minutes -= $cdr_cust_pkg_usage->minutes;
AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
"; }
+=item on_hold_sql
+
+Returns an SQL expression identifying on-hold packages.
+
+=cut
+
+sub on_hold_sql {
+ #$_[0]->recurring_sql(). ' AND '.
+ "
+ ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
+ AND cust_pkg.susp IS NOT NULL AND cust_pkg.susp != 0
+ AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 )
+ ";
+}
+
=item susp_sql
=item suspended_sql
"
( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
AND cust_pkg.susp IS NOT NULL AND cust_pkg.susp != 0
+ AND cust_pkg.setup IS NOT NULL AND cust_pkg.setup != 0
";
}
sub status_sql {
"CASE
WHEN cust_pkg.cancel IS NOT NULL THEN 'cancelled'
+ WHEN ( cust_pkg.susp IS NOT NULL AND cust_pkg.setup IS NULL ) THEN 'on hold'
WHEN cust_pkg.susp IS NOT NULL THEN 'suspended'
WHEN cust_pkg.setup IS NULL THEN 'not yet billed'
WHEN ".onetime_sql()." THEN 'one-time charge'