}
my @cust_svc = ();
+ my @cust_pkg_usage = ();
#foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
foreach my $cust_pkg ( $p->{'ncancelled'}
? $cust_main->ncancelled_pkgs
: $cust_main->unsuspended_pkgs ) {
next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
+ push @cust_pkg_usage, $cust_pkg->cust_pkg_usage;
}
@cust_svc = grep { $_->part_svc->selfservice_access ne 'hidden' } @cust_svc;
+ my %usage_pools;
+ foreach (@cust_pkg_usage) {
+ my $part = $_->part_pkg_usage;
+ my $tag = $part->description . ($part->shared ? 1 : 0);
+ my $row = $usage_pools{$tag}
+ ||= [ $part->description, 0, 0, $part->shared ? 1 : 0 ];
+ $row->[1] += $_->minutes; # minutes remaining
+ $row->[2] += $part->minutes; # minutes total
+ }
if ( $p->{'svcdb'} ) {
my $svcdb = ref($p->{'svcdb'}) eq 'HASH'
}
@cust_svc
],
+ 'usage_pools' => [
+ map { $usage_pools{$_} }
+ sort { $a cmp $b }
+ keys %usage_pools
+ ],
};
}
$cdr_type ? $cdr_type->cdrtypename : '';
},
},
+
+ {
+ 'key' => 'cdr-minutes_priority',
+ 'section' => 'telephony',
+ 'description' => 'Priority rule for assigning included minutes to CDRs.',
+ 'type' => 'select',
+ 'select_hash' => [
+ '' => 'No specific order',
+ 'time' => 'Chronological',
+ 'rate_high' => 'Highest rate first',
+ 'rate_low' => 'Lowest rate first',
+ ],
+ },
{
'key' => 'brand-agent',
use FS::GeocodeCache;
use FS::log;
use FS::log_context;
+ use FS::part_pkg_usage_class;
+ use FS::cust_pkg_usage;
+ use FS::part_pkg_usage_class;
+ use FS::part_pkg_usage;
+ use FS::cdr_cust_pkg_usage;
# Sammath Naur
if ( $FS::Mason::addl_handler_use ) {
'index' => [ [ 'pkgnum' ], [ 'discountnum' ], [ 'usernum' ], ],
},
+ 'cust_pkg_usage' => {
+ 'columns' => [
+ 'pkgusagenum', 'serial', '', '', '', '',
+ 'pkgnum', 'int', '', '', '', '',
+ 'minutes', 'int', '', '', '', '',
+ 'pkgusagepart', 'int', '', '', '', '',
+ ],
+ 'primary_key' => 'pkgusagenum',
+ 'unique' => [],
+ 'index' => [ [ 'pkgnum' ], [ 'pkgusagepart' ] ],
+ },
+
+ 'cdr_cust_pkg_usage' => {
+ 'columns' => [
+ 'cdrusagenum', 'bigserial', '', '', '', '',
+ 'acctid', 'bigint', '', '', '', '',
+ 'pkgusagenum', 'int', '', '', '', '',
+ 'minutes', 'int', '', '', '', '',
+ ],
+ 'primary_key' => 'cdrusagenum',
+ 'unique' => [],
+ 'index' => [ [ 'pkgusagenum' ], [ 'acctid' ] ],
+ },
+
'cust_bill_pkg_discount' => {
'columns' => [
'billpkgdiscountnum', 'serial', '', '', '', '',
'index' => [ [ 'disabled' ] ],
},
+ 'part_pkg_usage' => {
+ 'columns' => [
+ 'pkgusagepart', 'serial', '', '', '', '',
+ 'pkgpart', 'int', '', '', '', '',
+ 'minutes', 'int', '', '', '', '',
+ 'priority', 'int', 'NULL', '', '', '',
+ 'shared', 'char', 'NULL', 1, '', '',
+ 'rollover', 'char', 'NULL', 1, '', '',
+ 'description', 'varchar', 'NULL', $char_d, '', '',
+ ],
+ 'primary_key' => 'pkgusagepart',
+ 'unique' => [],
+ 'index' => [ [ 'pkgpart' ] ],
+ },
+
+ 'part_pkg_usage_class' => {
+ 'columns' => [
+ 'num', 'serial', '', '', '', '',
+ 'pkgusagepart', 'int', '', '', '', '',
+ 'classnum', 'int','NULL', '', '', '',
+ ],
+ 'primary_key' => 'num',
+ 'unique' => [ [ 'pkgusagepart', 'classnum' ] ],
+ 'index' => [],
+ },
+
'rate' => {
'columns' => [
'ratenum', 'serial', '', '', '', '',
Sets the status to the provided string. If there is an error, returns the
error, otherwise returns false.
+If status is being changed from 'rated' to some other status, also removes
+any usage allocations to this CDR.
+
=cut
sub set_status {
my($self, $status) = @_;
+ my $old_status = $self->freesidestatus;
$self->freesidestatus($status);
- $self->replace;
+ my $error = $self->replace;
+ if ( $old_status eq 'rated' and $status ne 'done' ) {
+ # deallocate any usage
+ foreach (qsearch('cdr_cust_pkg_usage', {acctid => $self->acctid})) {
+ my $cust_pkg_usage = $_->cust_pkg_usage;
+ $cust_pkg_usage->set('minutes', $cust_pkg_usage->minutes + $_->minutes);
+ $error ||= $cust_pkg_usage->replace || $_->delete;
+ }
+ }
+ $error;
}
=item set_status_and_rated_price STATUS RATED_PRICE [ SVCNUM [ OPTION => VALUE ... ] ]
rated minutes of this CDR.
region_group_included_minutes_hashref is required for prefix price plans which
-have included minues (otehrwise unused/ignored). It should be set to an empty
+have included minues (otherwise unused/ignored). It should be set to an empty
hashref at the start of a month's rating and then preserved across CDRs.
=cut
sub rate_prefix {
my( $self, %opt ) = @_;
my $part_pkg = $opt{'part_pkg'} or return "No part_pkg specified";
+ my $cust_pkg = $opt{'cust_pkg'};
my $da_rewrote = 0;
# this will result in those CDRs being marked as done... is that
$seconds_left -= $charge_sec;
- my $included_min = $opt{'region_group_included_min_hashref'} || {};
-
- $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included
- unless exists $included_min->{$regionnum}{$ratetimenum};
-
my $granularity = $rate_detail->sec_granularity;
my $minutes;
$seconds += $charge_sec;
+ if ( $rate_detail->min_included ) {
+ # the old, kind of deprecated way to do this
+ my $included_min = $opt{'region_group_included_min_hashref'} || {};
- my $region_group = ($part_pkg->option_cacheable('min_included') || 0) > 0;
+ # by default, set the included minutes for this region/time to
+ # what's in the rate_detail
+ $included_min->{$regionnum}{$ratetimenum} = $rate_detail->min_included
+ unless exists $included_min->{$regionnum}{$ratetimenum};
- ${$opt{region_group_included_min}} -= $minutes
- if $region_group && $rate_detail->region_group;
+ # the way that doesn't work
+ #my $region_group = ($part_pkg->option_cacheable('min_included') || 0) > 0;
+
+ #${$opt{region_group_included_min}} -= $minutes
+ # if $region_group && $rate_detail->region_group;
+
+ if ( $included_min->{$regionnum}{$ratetimenum} > $minutes ) {
+ $charge_sec = 0;
+ $included_min->{$regionnum}{$ratetimenum} -= $minutes;
+ } else {
+ $charge_sec -= ($included_min->{$regionnum}{$ratetimenum} * 60);
+ $included_min->{$regionnum}{$ratetimenum} = 0;
+ }
+ } else {
+ # the new way!
+ my $applied_min = $cust_pkg->apply_usage(
+ 'cdr' => $self,
+ 'rate_detail' => $rate_detail,
+ 'minutes' => $minutes
+ );
+ # for now, usage pools deal only in whole minutes
+ $charge_sec -= $applied_min * 60;
+ }
- $included_min->{$regionnum}{$ratetimenum} -= $minutes;
- if (
- $included_min->{$regionnum}{$ratetimenum} <= 0
- && ( ${$opt{region_group_included_min}} <= 0
- || ! $rate_detail->region_group
- )
- )
- {
+ if ( $charge_sec > 0 ) {
#NOW do connection charges here... right?
#my $conn_seconds = min($seconds_left, $rate_detail->conn_sec);
}
#should preserve (display?) this
- my $charge_min = 0 - $included_min->{$regionnum}{$ratetimenum} - ( $conn_seconds / 60 );
- $included_min->{$regionnum}{$ratetimenum} = 0;
+ my $charge_min = ( $charge_sec - $conn_seconds ) / 60;
$charge += ($rate_detail->min_charge * $charge_min) if $charge_min > 0; #still not rounded
- } elsif ( ${$opt{region_group_included_min}} > 0
- && $region_group
- && $rate_detail->region_group
- )
- {
- $included_min->{$regionnum}{$ratetimenum} = 0
}
# choose next rate_detail
--- /dev/null
+package FS::cdr_cust_pkg_usage;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::cdr_cust_pkg_usage - Object methods for cdr_cust_pkg_usage records
+
+=head1 SYNOPSIS
+
+ use FS::cdr_cust_pkg_usage;
+
+ $record = new FS::cdr_cust_pkg_usage \%hash;
+ $record = new FS::cdr_cust_pkg_usage { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cdr_cust_pkg_usage object represents an allocation of included
+usage minutes to a call. FS::cdr_cust_pkg_usage inherits from
+FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item cdrusagenum - primary key
+
+=item acctid - foreign key to cdr.acctid
+
+=item pkgusagenum - foreign key to cust_pkg_usage.pkgusagenum
+
+=item minutes - the number of minutes allocated
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example. To add the example 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 { 'cdr_cust_pkg_usage'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=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.
+
+=item check
+
+Checks all fields to make sure this is a valid example. 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('cdrusagenum')
+ || $self->ut_foreign_key('acctid', 'cdr', 'acctid')
+ || $self->ut_foreign_key('pkgusagenum', 'cust_pkg_usage', 'pkgusagenum')
+ || $self->ut_number('minutes')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item cust_pkg_usage
+
+Returns the L<FS::cust_pkg_usage> object that this usage allocation came from.
+
+=item cdr
+
+Returns the L<FS::cdr> object that the usage was applied to.
+
+=cut
+
+sub cust_pkg_usage {
+ FS::cust_pkg_usage->by_key($_[0]->pkgusagenum);
+}
+
+sub cdr {
+ FS::cdr->by_key($_[0]->acctid);
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
use vars qw($disable_agentcheck $DEBUG $me);
use Carp qw(cluck);
use Scalar::Util qw( blessed );
-use List::Util qw(max);
+use List::Util qw(min max);
use Tie::IxHash;
use Time::Local qw( timelocal timelocal_nocheck );
use MIME::Entity;
use FS::pkg_svc;
use FS::cust_bill_pkg;
use FS::cust_pkg_detail;
+use FS::cust_pkg_usage;
+use FS::cdr_cust_pkg_usage;
use FS::cust_event;
use FS::h_cust_svc;
use FS::reg_code;
}
}
+ foreach my $usage ( $self->cust_pkg_usage ) {
+ $error = $usage->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "deleting usage pools: $error";
+ }
+ }
+
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
return '' if $date; #no errors
$dbh->rollback if $oldAutoCommit;
return "Error setting usage values: $error";
}
+ } else {
+ # if NOT changing pkgpart, transfer any usage pools over
+ foreach my $usage ($self->cust_pkg_usage) {
+ $usage->set('pkgnum', $cust_pkg->pkgnum);
+ $error = $usage->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error transferring usage pools: $error";
+ }
+ }
}
# Order any supplemental packages.
grep { $_->status eq 'active' } $self->cust_pkg_discount;
}
-=back
+=item cust_pkg_usage
+
+Returns a list of all voice usage counters attached to this package.
+
+=cut
+
+sub cust_pkg_usage {
+ my $self = shift;
+ qsearch('cust_pkg_usage', { pkgnum => $self->pkgnum });
+}
+
+=item apply_usage OPTIONS
+
+Takes the following options:
+- cdr: a call detail record (L<FS::cdr>)
+- rate_detail: the rate determined for this call (L<FS::rate_detail>)
+- minutes: the maximum number of minutes to be charged
+
+Finds available usage minutes for a call of this class, and subtracts
+up to that many minutes from the usage pool. If the usage pool is empty,
+and the C<cdr-minutes_priority> global config option is set, minutes may
+be taken from other calls as well. Either way, an allocation record will
+be created (L<FS::cdr_cust_pkg_usage>) and this method will return the
+number of minutes of usage applied to the call.
+
+=cut
+
+sub apply_usage {
+ my ($self, %opt) = @_;
+ my $cdr = $opt{cdr};
+ my $rate_detail = $opt{rate_detail};
+ my $minutes = $opt{minutes};
+ my $classnum = $rate_detail->classnum;
+ my $pkgnum = $self->pkgnum;
+ my $custnum = $self->custnum;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+ my $order = FS::Conf->new->config('cdr-minutes_priority');
+
+ my @usage_recs = qsearch({
+ 'table' => 'cust_pkg_usage',
+ 'addl_from' => ' JOIN part_pkg_usage USING (pkgusagepart)'.
+ ' JOIN cust_pkg USING (pkgnum)'.
+ ' JOIN part_pkg_usage_class USING (pkgusagepart)',
+ 'select' => 'cust_pkg_usage.*',
+ 'extra_sql' => " WHERE ( cust_pkg.pkgnum = $pkgnum OR ".
+ " ( cust_pkg.custnum = $custnum AND ".
+ " part_pkg_usage.shared IS NOT NULL ) ) AND ".
+ " part_pkg_usage_class.classnum = $classnum AND ".
+ " cust_pkg_usage.minutes > 0",
+ 'order_by' => " ORDER BY priority ASC",
+ });
+
+ my $orig_minutes = $minutes;
+ my $error;
+ while (!$error and $minutes > 0 and @usage_recs) {
+ my $cust_pkg_usage = shift @usage_recs;
+ $cust_pkg_usage->select_for_update;
+ my $cdr_cust_pkg_usage = FS::cdr_cust_pkg_usage->new({
+ pkgusagenum => $cust_pkg_usage->pkgusagenum,
+ acctid => $cdr->acctid,
+ minutes => min($cust_pkg_usage->minutes, $minutes),
+ });
+ $cust_pkg_usage->set('minutes',
+ sprintf('%.0f', $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;
+ }
+ if ( $order and $minutes > 0 and !$error ) {
+ # then try to steal minutes from another call
+ my %search = (
+ 'table' => 'cdr_cust_pkg_usage',
+ 'addl_from' => ' JOIN cust_pkg_usage USING (pkgusagenum)'.
+ ' JOIN part_pkg_usage USING (pkgusagepart)'.
+ ' JOIN cust_pkg USING (pkgnum)'.
+ ' JOIN part_pkg_usage_class USING (pkgusagepart)'.
+ ' JOIN cdr USING (acctid)',
+ 'select' => 'cdr_cust_pkg_usage.*',
+ 'extra_sql' => " WHERE cdr.freesidestatus = 'rated' AND ".
+ " ( cust_pkg.pkgnum = $pkgnum OR ".
+ " ( cust_pkg.custnum = $custnum AND ".
+ " part_pkg_usage.shared IS NOT NULL ) ) AND ".
+ " part_pkg_usage_class.classnum = $classnum",
+ 'order_by' => ' ORDER BY part_pkg_usage.priority ASC',
+ );
+ if ( $order eq 'time' ) {
+ # find CDRs that are using minutes, but have a later startdate
+ # than this call
+ my $startdate = $cdr->startdate;
+ if ($startdate !~ /^\d+$/) {
+ die "bad cdr startdate '$startdate'";
+ }
+ $search{'extra_sql'} .= " AND cdr.startdate > $startdate";
+ # minimize needless reshuffling
+ $search{'order_by'} .= ', cdr.startdate DESC';
+ } else {
+ # XXX may not work correctly with rate_time schedules. Could
+ # fix this by storing ratedetailnum in cdr_cust_pkg_usage, I
+ # think...
+ $search{'addl_from'} .=
+ ' JOIN rate_detail'.
+ ' ON (cdr.rated_ratedetailnum = rate_detail.ratedetailnum)';
+ if ( $order eq 'rate_high' ) {
+ $search{'extra_sql'} .= ' AND rate_detail.min_charge < '.
+ $rate_detail->min_charge;
+ $search{'order_by'} .= ', rate_detail.min_charge ASC';
+ } elsif ( $order eq 'rate_low' ) {
+ $search{'extra_sql'} .= ' AND rate_detail.min_charge > '.
+ $rate_detail->min_charge;
+ $search{'order_by'} .= ', rate_detail.min_charge DESC';
+ } else {
+ # this should really never happen
+ die "invalid cdr-minutes_priority value '$order'\n";
+ }
+ }
+ my @cdr_usage_recs = qsearch(\%search);
+ my %reproc_cdrs;
+ while (!$error and @cdr_usage_recs and $minutes > 0) {
+ my $cdr_cust_pkg_usage = shift @cdr_usage_recs;
+ my $cust_pkg_usage = $cdr_cust_pkg_usage->cust_pkg_usage;
+ my $old_cdr = $cdr_cust_pkg_usage->cdr;
+ $reproc_cdrs{$old_cdr->acctid} = $old_cdr;
+ $cdr_cust_pkg_usage->select_for_update;
+ $old_cdr->select_for_update;
+ $cust_pkg_usage->select_for_update;
+ # in case someone else stole the usage from this CDR
+ # while waiting for the lock...
+ next if $old_cdr->acctid != $cdr_cust_pkg_usage->acctid;
+ # steal the usage allocation and flag the old CDR for reprocessing
+ $cdr_cust_pkg_usage->set('acctid', $cdr->acctid);
+ # if the allocation is more minutes than we need, adjust it...
+ my $delta = $cdr_cust_pkg_usage->minutes - $minutes;
+ if ( $delta > 0 ) {
+ $cdr_cust_pkg_usage->set('minutes', $minutes);
+ $cust_pkg_usage->set('minutes', $cust_pkg_usage->minutes + $delta);
+ $error = $cust_pkg_usage->replace;
+ }
+ warn 'CDR '.$cdr->acctid . ' stealing allocation '.$cdr_cust_pkg_usage->cdrusagenum.' from CDR '.$old_cdr->acctid."\n";
+ $error ||= $cdr_cust_pkg_usage->replace;
+ # deduct the stolen minutes
+ $minutes -= $cdr_cust_pkg_usage->minutes;
+ }
+ # after all minute-stealing is done, reset the affected CDRs
+ foreach (values %reproc_cdrs) {
+ $error ||= $_->set_status('');
+ # XXX or should we just call $cdr->rate right here?
+ # it's not like we can create a loop this way, since the min_charge
+ # or call time has to go monotonically in one direction.
+ # we COULD get some very deep recursions going, though...
+ }
+ } # if $order and $minutes
+ if ( $error ) {
+ $dbh->rollback;
+ die "error applying included minutes\npkgnum ".$self->pkgnum.", class $classnum, acctid ".$cdr->acctid."\n$error\n"
+ } else {
+ $dbh->commit if $oldAutoCommit;
+ return $orig_minutes - $minutes;
+ }
+}
=item supplemental_pkgs
return;
}
+=back
+
=head1 CLASS METHODS
=over 4
--- /dev/null
+package FS::cust_pkg_usage;
+
+use strict;
+use base qw( FS::Record );
+use FS::cust_pkg;
+use FS::part_pkg_usage;
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::cust_pkg_usage - Object methods for cust_pkg_usage records
+
+=head1 SYNOPSIS
+
+ use FS::cust_pkg_usage;
+
+ $record = new FS::cust_pkg_usage \%hash;
+ $record = new FS::cust_pkg_usage { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_pkg_usage object represents a counter of remaining included
+minutes on a voice-call package. FS::cust_pkg_usage inherits from
+FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item pkgusagenum - primary key
+
+=item pkgnum - the package (L<FS::cust_pkg>) containing the usage
+
+=item pkgusagepart - the usage stock definition (L<FS::part_pkg_usage>).
+This record in turn links to the call usage classes that are eligible to
+use these minutes.
+
+=item minutes - the remaining minutes
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+=cut
+
+sub table { 'cust_pkg_usage'; }
+
+=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
+
+sub delete {
+ my $self = shift;
+ my $error = $self->reset || $self->SUPER::delete;
+}
+
+=item reset
+
+Remove all allocations of this usage to CDRs.
+
+=cut
+
+sub reset {
+ my $self = shift;
+ my $error = '';
+ foreach (qsearch('cdr_cust_pkg_usage', { pkgusagenum => $self->pkgusagenum }))
+ {
+ $error ||= $_->delete;
+ }
+ $error;
+}
+
+=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 example. 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('pkgusagenum')
+ || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum')
+ || $self->ut_numbern('minutes')
+ || $self->ut_foreign_key('pkgusagepart', 'part_pkg_usage', 'pkgusagepart')
+ ;
+ return $error if $error;
+
+ if ( $self->minutes eq '' ) {
+ $self->set(minutes => $self->part_pkg_usage->minutes);
+ }
+
+ $self->SUPER::check;
+}
+
+=item cust_pkg
+
+Return the L<FS::cust_pkg> linked to this record.
+
+=item part_pkg_usage
+
+Return the L<FS::part_pkg_usage> linked to this record.
+
+=cut
+
+sub cust_pkg {
+ my $self = shift;
+ FS::cust_pkg->by_key($self->pkgnum);
+}
+
+sub part_pkg_usage {
+ my $self = shift;
+ FS::part_pkg_usage->by_key($self->pkgusagepart);
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
use FS::part_pkg_taxproduct;
use FS::part_pkg_link;
use FS::part_pkg_discount;
+use FS::part_pkg_usage;
use FS::part_pkg_vendor;
@ISA = qw( FS::m2m_Common FS::option_Common );
qsearch('part_pkg_discount', { 'pkgpart' => $self->pkgpart });
}
+=item part_pkg_usage
+
+Returns the voice usage pools (see L<FS::part_pkg_usage>) defined for
+this package.
+
+=cut
+
+sub part_pkg_usage {
+ my $self = shift;
+ qsearch('part_pkg_usage', { 'pkgpart' => $self->pkgpart });
+}
+
=item _rebless
Reblesses the object into the FS::part_pkg::PLAN class (if available), where
my $error = $cdr->rate(
'part_pkg' => $self,
+ 'cust_pkg' => $cust_pkg,
'svcnum' => $svc_x->svcnum,
'single_price_included_min' => \$included_min,
'region_group_included_min' => \$region_group_included_min,
$count;
}
+sub reset_usage {
+ my ($self, $cust_pkg, %opt) = @_;
+ my @part_pkg_usage = $self->part_pkg_usage or return '';
+ warn " resetting usage minutes\n" if $opt{debug};
+ my %cust_pkg_usage = map { $_->pkgusagepart, $_ } $cust_pkg->cust_pkg_usage;
+ foreach my $part_pkg_usage (@part_pkg_usage) {
+ my $part = $part_pkg_usage->pkgusagepart;
+ my $usage = $cust_pkg_usage{$part} ||
+ FS::cust_pkg_usage->new({
+ 'pkgnum' => $cust_pkg->pkgnum,
+ 'pkgusagepart' => $part,
+ 'minutes' => $part_pkg_usage->minutes,
+ });
+ foreach my $cdr_usage (
+ qsearch('cdr_cust_pkg_usage', {'cdrusagenum' => $usage->cdrusagenum})
+ ) {
+ my $error = $cdr_usage->delete;
+ warn " error resetting CDR usage: $error\n";
+ }
+
+ if ( $usage->pkgusagenum ) {
+ if ( $part_pkg_usage->rollover ) {
+ $usage->set('minutes', $part_pkg_usage->minutes + $usage->minutes);
+ } else {
+ $usage->set('minutes', $part_pkg_usage->minutes);
+ }
+ my $error = $usage->replace;
+ warn " error resetting usage minutes: $error\n" if $error;
+ } else {
+ my $error = $usage->insert;
+ warn " error resetting usage minutes: $error\n" if $error;
+ }
+ } #foreach $part_pkg_usage
+}
+
# tells whether cust_bill_pkg_detail should return a single line for
# each phonenum
sub sum_usage {
--- /dev/null
+package FS::part_pkg_usage;
+
+use strict;
+use base qw( FS::m2m_Common FS::Record );
+use FS::Record qw( qsearch qsearchs );
+use Scalar::Util qw(blessed);
+
+=head1 NAME
+
+FS::part_pkg_usage - Object methods for part_pkg_usage records
+
+=head1 SYNOPSIS
+
+ use FS::part_pkg_usage;
+
+ $record = new FS::part_pkg_usage \%hash;
+ $record = new FS::part_pkg_usage { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_usage object represents a stock of usage minutes (generally
+for voice services) included in a package definition. FS::part_pkg_usage
+inherits from FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item pkgusagepart - primary key
+
+=item pkgpart - the package definition (L<FS::part_pkg>)
+
+=item minutes - the number of minutes included per billing cycle
+
+=item priority - the relative order in which to use this stock of minutes.
+
+=item shared - 'Y' to allow these minutes to be shared with other packages
+belonging to the same customer. Otherwise, only usage allocated to this
+package will use this stock of minutes.
+
+=item rollover - 'Y' to allow unused minutes to carry over between billing
+cycles. Otherwise, the available minutes will reset to the value of the
+"minutes" field upon billing.
+
+=item description - a text description of this stock of minutes
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+=item insert CLASSES
+
+=item replace CLASSES
+
+CLASSES can be an array or hash of usage classnums (see L<FS::usage_class>)
+to link to this record.
+
+=item delete
+
+=cut
+
+sub table { 'part_pkg_usage'; }
+
+sub insert {
+ my $self = shift;
+ my $opt = ref($_[0]) eq 'HASH' ? shift : { @_ };
+
+ $self->SUPER::insert
+ || $self->process_m2m( 'link_table' => 'part_pkg_usage_class',
+ 'target_table' => 'usage_class',
+ 'params' => $opt,
+ );
+}
+
+sub replace {
+ my $self = shift;
+ my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+ ? shift
+ : $self->replace_old;
+ my $opt = ref($_[0]) eq 'HASH' ? $_[0] : { @_ };
+ $self->SUPER::replace($old)
+ || $self->process_m2m( 'link_table' => 'part_pkg_usage_class',
+ 'target_table' => 'usage_class',
+ 'params' => $opt,
+ );
+}
+
+sub delete {
+ my $self = shift;
+ $self->process_m2m( 'link_table' => 'part_pkg_usage_class',
+ 'target_table' => 'usage_class',
+ 'params' => {},
+ ) || $self->SUPER::delete;
+}
+
+=item check
+
+Checks all fields to make sure this is a valid example. 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('pkgusagepart')
+ || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart')
+ || $self->ut_number('minutes')
+ || $self->ut_numbern('priority')
+ || $self->ut_flag('shared')
+ || $self->ut_flag('rollover')
+ || $self->ut_textn('description')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item classnums
+
+Returns the usage class numbers that are allowed to use minutes from this
+pool.
+
+=cut
+
+sub classnums {
+ my $self = shift;
+ if (!$self->get('classnums')) {
+ my $classnums = [
+ map { $_->classnum }
+ qsearch('part_pkg_usage_class', { 'pkgusagepart' => $self->pkgusagepart })
+ ];
+ $self->set('classnums', $classnums);
+ }
+ @{ $self->get('classnums') };
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
--- /dev/null
+package FS::part_pkg_usage_class;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::part_pkg_usage_class - Object methods for part_pkg_usage_class records
+
+=head1 SYNOPSIS
+
+ use FS::part_pkg_usage_class;
+
+ $record = new FS::part_pkg_usage_class \%hash;
+ $record = new FS::part_pkg_usage_class { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_usage_class object is a link between a package usage stock
+(L<FS::part_pkg_usage>) and a voice usage class (L<FS::usage_class)>.
+FS::part_pkg_usage_class inherits from FS::Record. The following fields
+are currently supported:
+
+=over 4
+
+=item num - primary key
+
+=item pkgusagepart - L<FS::part_pkg_usage> key
+
+=item classnum - L<FS::usage_class> key. Set to null to allow this stock
+to be used for calls that have no usage class. To avoid confusion, you
+should only do this if you don't use usage classes on your system.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example. To add the example 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_pkg_usage_class'; }
+
+=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('num')
+ || $self->ut_foreign_key('pkgusagepart', 'part_pkg_usage', 'pkgusagepart')
+ || $self->ut_foreign_keyn('classnum', 'usage_class', 'classnum')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+The author forgot to customize this manpage.
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
t/log.t
FS/log_context.pm
t/log_context.t
+FS/part_pkg_usage_class.pm
+t/part_pkg_usage_class.t
+FS/cust_pkg_usage.pm
+t/cust_pkg_usage.t
+FS/part_pkg_usage_class.pm
+t/part_pkg_usage_class.t
+FS/part_pkg_usage.pm
+t/part_pkg_usage.t
+FS/cdr_cust_pkg_usage.pm
+t/cdr_cust_pkg_usage.t
$extra_sql .= ' AND cdrtypenum IN ('. join(',', @cdrtypenums ). ')';
}
-our %svcnum = ();
-our %pkgpart = ();
-our %part_pkg = ();
+our %svcnum = (); # phonenum => svcnum
+our %pkgnum = (); # phonenum => pkgnum
+our %cust_pkg = (); # pkgnum => cust_pkg (NOT phonenum => cust_pkg!)
+our %pkgpart = (); # phonenum => pkgpart
+our %part_pkg = (); # phonenum => part_pkg
#some false laziness w/freeside-cdrrewrited
next;
}
+ $pkgnum{$number} = $cust_pkg->pkgnum;
+ $cust_pkg{$cust_pkg->pkgnum} ||= $cust_pkg;
+
#get the package, search through the part_pkg and linked for a voip_cdr def w/matching cdrtypenum (or no use_cdrtypenum)
my @part_pkg =
grep { $_->plan eq 'voip_cdr'
#}
#XXX if $part_pkg->option('min_included') then we can't prerate this CDR
-
+
my $error = $cdr->rate(
'part_pkg' => $part_pkg{ $pkgpart{$number} },
- 'svcnum' => $svcnum{ $number },
+ 'cust_pkg' => $cust_pkg{ $pkgnum{$number} },
+ 'svcnum' => $svcnum{$number},
);
if ( $error ) {
#XXX ???
--- /dev/null
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cdr_cust_pkg_usage;
+$loaded=1;
+print "ok 1\n";
--- /dev/null
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_pkg_usage;
+$loaded=1;
+print "ok 1\n";
--- /dev/null
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pkg_usage;
+$loaded=1;
+print "ok 1\n";
--- /dev/null
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pkg_usage_class;
+$loaded=1;
+print "ok 1\n";
<%= $url = "$selfurl?session=$session_id;action=";
- @svc_acct = grep { $_->{svcdb} eq 'svc_acct' } @svcs;
- @svc_phone = grep { $_->{svcdb} eq 'svc_phone' } @svcs;
- @svc_port = grep { $_->{svcdb} eq 'svc_port' } @svcs;
+ %by_pkg_label = (); # not used yet, but I'm sure it will be...
+ @svc_acct = ();
+ @svc_phone = ();
+ @svc_port = ();
+
+ foreach (@svcs) {
+ $by_pkg_label{ $_->{pkg_label} } ||= [];
+ push @{ $by_pkg_label{ $_->{pkg_label} } }, $_;
+ if ( $_->{svcdb} eq 'svc_acct' ) {
+ push @svc_acct, $_;
+ } elsif ( $_->{svcdb} eq 'svc_phone' ) {
+ push @svc_phone, $_;
+ } elsif ( $_->{svcdb} eq 'svc_port' ) {
+ push @svc_port, $_;
+ }
+ }
'';
%>
<%= include('header', 'Account usage') %>
$any{$dir} = grep { $_->{$dir} } @svc_phone;
}
$OUT.= '<FONT SIZE="4">Call usage</FONT><BR><BR>
- <TABLE BGCOLOR="#cccccc">
+ <TABLE BGCOLOR="#cccccc" STYLE="display:inline-block">
<TR>
<TH ALIGN="left">Number</TH>';
if ( $any{outbound} ) {
'';
%>
-<%= scalar(@svc_phone) ? '</TABLE><BR><BR>' : '' %>
+<%= if ( @usage_pools ) {
+ $OUT .= '</TABLE>
+ <TABLE BGCOLOR="#cccccc" STYLE="display: inline-block">
+ <TR><TH COLSPAN=4>Remaining minutes</TH></TR>
+ ';
+ my $any_shared = 0;
+ foreach my $usage (@usage_pools) {
+ # false laziness with the back office side
+ my ($description, $remain, $total, $shared) = @$usage;
+ if ( $shared ) {
+ $any_shared = 1;
+ $description .= '*';
+ }
+ my $ratio = 255 * ($remain/$total);
+ $ratio = 255 if $color > 255;
+ my $color =
+ sprintf('STYLE="font-weight: bold; color: #%02x%02x00"',
+ 255 - $ratio, $ratio);
+ $OUT .=
+ qq!<TR>
+ <TD ALIGN="right">$description</TD>
+ <TD $color ALIGN="right">$remain</TD>
+ <TD $color> / </TD>
+ <TD $color>$total</TD>
+ </TR>!;
+ }
+ if ( $any_shared ) {
+ $OUT .= '<TR STYLE="font-size: 80%; font-style: italic">'.
+ '<TD COLSPAN=4>* shared among all your phone plans</TD></TR>';
+ }
+}
+if ( scalar(@svc_phone) or scalar(@usage_pools) ) {
+ $OUT .= '</TABLE><BR><BR>';
+}
+'';
+%>
<%= if ( @svc_port ) {
$OUT.= '<FONT SIZE="4">Bandwidth Graphs</FONT><BR><BR>
<% include( 'elements/browse.html',
'title' => 'Package Definitions',
+ 'menubar' => \@menubar,
'html_init' => $html_init,
'html_form' => $html_form,
'html_posttotal' => $html_posttotal,
sub {
my $part_pkg = shift;
+ my @part_pkg_usage = sort { $a->priority <=> $b->priority }
+ $part_pkg->part_pkg_usage;
[
(map {
]
}
$part_pkg->svc_part_pkg_link
- )
+ ),
+ ( scalar(@part_pkg_usage) ?
+ [ { data => 'Usage minutes',
+ align => 'center',
+ colspan => 2,
+ data_style => 'b',
+ link => $p.'browse/part_pkg_usage.html#pkgpart'.
+ $part_pkg->pkgpart
+ } ]
+ : ()
+ ),
+ ( map {
+ [ { data => $_->minutes,
+ align => 'right'
+ },
+ { data => $_->description,
+ align => 'left'
+ },
+ ]
+ } @part_pkg_usage
+ ),
];
};
) . '</FORM>';
}
+my @menubar;
+# show this if there are any voip_cdr packages defined
+if ( FS::part_pkg->count("plan = 'voip_cdr'") ) {
+ push @menubar, 'Per-package usage minutes' => $p.'browse/part_pkg_usage.html';
+}
</%init>
--- /dev/null
+<& /elements/header.html, 'Package usage minutes' &>
+<& /elements/menubar.html, 'Package definitions', $p.'browse/part_pkg.cgi' &>
+<STYLE TYPE="text/css">
+.pkg_head {
+ background-color: #dddddd;
+ font-style: italic;
+}
+.pkg_head > td {
+ border-style: solid;
+ border-radius: 3px;
+ border-color: #555555;
+ border-width: 1px;
+}
+.usage > td {
+ text-align: center;
+}
+.error {
+ color: #ff0000;
+}
+</STYLE>
+<FORM METHOD="POST" ACTION="<%$fsurl%>edit/process/part_pkg_usage.html">
+ <TABLE STYLE="margin-top: 1em">
+ <TR>
+ <TH>Minutes</TH>
+ <TH>Shared</TH>
+ <TH>Rollover</TH>
+ <TH>Description</TH>
+ <TH>Priority</TH>
+% foreach my $class (@usage_class) {
+ <TH><% $class->classname %></TH>
+% }
+ </TR>
+
+% my $error = $cgi->param('error');
+% foreach my $part_pkg (@part_pkg) {
+% my $pkgpart = $part_pkg->pkgpart;
+% my @part_pkg_usage;
+% if ( $error ) {
+% @part_pkg_usage = @{ $error->{$pkgpart} };
+% } else {
+% @part_pkg_usage = $part_pkg->part_pkg_usage;
+% foreach my $usage (@part_pkg_usage) {
+% foreach ($usage->classnums) {
+% $usage->set("class$_".'_', 'Y');
+% }
+% }
+% }
+ <TR CLASS="pkg_head" ID="pkgpart<%$pkgpart%>">
+ <TD COLSPAN=<%$n_cols%>><% $part_pkg->pkg_comment %></TD>
+% # make it easy to enumerate the pkgparts later
+ <INPUT TYPE="hidden" NAME="pkgpart" VALUE="<% $pkgpart %>">
+ </TR>
+% # template row
+ <TR id="pkgpart<%$pkgpart%>_template" CLASS="usage">
+ <TD>
+ <INPUT TYPE="hidden" NAME="pkgusagepart">
+ <INPUT TYPE="text" NAME="minutes" ID="minutes" SIZE=7>
+ </TD>
+% foreach (qw(shared rollover)) {
+ <TD>
+ <INPUT TYPE="checkbox" NAME="<% $_ %>" ID="<% $_ %>" VALUE="Y">
+ </TD>
+% }
+ <TD>
+ <INPUT TYPE="text" NAME="description" ID="description" SIZE=20>
+ </TD>
+ <TD>
+ <INPUT TYPE="text" NAME="priority" ID="priority" SIZE=3>
+ </TD>
+% foreach (@usage_class) {
+% my $classnum = 'class' . $_->classnum . '_';
+ <TD>
+ <INPUT TYPE="checkbox" NAME="<% $classnum %>" ID="<% $classnum %>" VALUE="Y">
+ </TD>
+% }
+ </TR>
+ <& /elements/auto-table.html,
+ table => "pkgpart$pkgpart",
+ template_row => "pkgpart$pkgpart".'_template',
+ data => \@part_pkg_usage,
+ &>
+% }
+ </TABLE>
+ <BR>
+ <INPUT TYPE="submit">
+</FORM>
+<& /elements/footer.html &>
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied"
+ unless $curuser->access_right(
+ ['Edit package definitions', 'Edit global package definitions']
+ );
+
+my @where = ("(plan = 'voip_cdr' OR plan = 'voip_inbound')",
+ "freq != '0'",
+ "disabled IS NULL");
+push @where, FS::part_pkg->curuser_pkgs_sql
+ unless $curuser->access_right('Edit global package definitions');
+my $extra_sql = ' WHERE '.join(' AND ', @where);
+my @part_pkg = qsearch({
+ 'table' => 'part_pkg',
+ 'extra_sql' => $extra_sql,
+ 'order_by' => ' ORDER BY pkgpart',
+});
+
+my @usage_class = sort { $a->weight <=> $b->weight }
+ qsearch('usage_class', { disabled => '' });
+
+my $n_usage_classes = scalar(@usage_class);
+my $n_cols = $n_usage_classes + 5; # minutes, shared, rollover, desc, prio
+</%init>
--- /dev/null
+% if ( $is_error ) {
+% $cgi->param('error' => \%part_pkg_usage);
+% # internal redirect, because it's a lot of state to pass through
+<& /browse/part_pkg_usage.html &>
+% } else {
+% # uh, not quite sure...
+<% $cgi->redirect($fsurl.'browse/part_pkg.cgi') %>
+% }
+<%init>
+my %vars = $cgi->Vars;
+my %part_pkg_usage;
+my $is_error;
+foreach my $pkgpart ($cgi->param('pkgpart')) {
+ next unless $pkgpart =~ /^\d+$/;
+ my $part_pkg = FS::part_pkg->by_key($pkgpart)
+ or die "unknown pkgpart $pkgpart";
+ my %old = map { $_->pkgusagepart => $_ } $part_pkg->part_pkg_usage;
+ $part_pkg_usage{$pkgpart} ||= [];
+ my @rows;
+ foreach (grep /^pkgpart$pkgpart/, keys %vars) {
+ /^pkgpart\d+_(\w+\D)(\d+)$/ or die "misspelled field name '$_'";
+ my $value = delete $vars{$_};
+ my $field = $1;
+ my $row = $2;
+ $rows[$row] ||= {};
+ $rows[$row]->{$field} = $value;
+ }
+
+ foreach my $row (@rows) {
+ next if !defined($row);
+ my $error;
+ my %classes;
+ foreach my $class (grep /^class/, keys %$row) {
+ $class =~ /^class(\d+)_$/;
+ my $classnum = $1;
+ $classes{$classnum} = delete $row->{$class};
+ }
+ my $usage = FS::part_pkg_usage->new($row);
+ $usage->set('pkgpart', $pkgpart);
+ if ( $usage->pkgusagepart and $row->{minutes} > 0 ) {
+ $error = $usage->replace(\%classes);
+ # and don't delete the existing one
+ delete($old{$usage->pkgusagepart});
+ } elsif ( $row->{minutes} > 0 ) {
+ $error = $usage->insert(\%classes);
+ } else {
+ next;
+ }
+ if ( $error ) {
+ $usage->set('error', $error);
+ $is_error = 1;
+ }
+ push @{ $part_pkg_usage{$pkgpart} }, $usage;
+ }
+
+ foreach my $usage (values %old) {
+ # all of these were not sent back by the client, so delete them
+ my $error = $usage->delete;
+ if ( $error ) {
+ $usage->set('error', $error);
+ $is_error = 1;
+ unshift @{ $part_pkg_usage{$pkgpart} }, $usage;
+ }
+ }
+
+}
+</%init>
if ( obj.id ) {
obj.id = obj.id + rownum;
}
- if ( obj.name ) {
- obj.name = obj.name + rownum;
+ if ( obj.getAttribute('name') ) {
+ obj.setAttribute('name', obj.getAttribute('name') + rownum);
// also, in this case it's a form field that will be part of the record
// so set up an onchange handler
obj.onchange = <%$pre%>possiblyAddRow_factory(obj);
<%$pre%>set_rownum(row, this_rownum);
if(data instanceof Array) {
for (i = 0; i < data.length && i < <%$pre%>fieldorder.length; i++) {
- var el = document.getElementsByName(<%$pre%>fieldorder[i] + this_rownum)[0];
+ var el = document.getElementsByName(<%$pre |js_string%> +
+ <%$pre%>fieldorder[i] +
+ this_rownum)[0];
if (el) {
- el.value = data[i];
+ if ( el.tagName.toLowerCase() == 'span' ) {
+ el.innerHTML = data[i];
+ } else if ( el.type == 'checkbox' ) {
+ el.checked = (el.value == data[i]);
+ } else {
+ el.value = data[i];
+ }
}
}
} else if (data instanceof Object) {
for (var field in data) {
- var el = document.getElementsByName(field + this_rownum)[0];
+ var el = document.getElementsByName(<%$pre |js_string%> +
+ field +
+ this_rownum)[0];
if (el) {
- el.value = data[field];
-% # doesn't work for checkbox
+ if ( el.tagName.toLowerCase() == 'span' ) {
+ el.innerHTML = data[field];
+ } else if ( el.type == 'checkbox' ) {
+ el.checked = (el.value == data[field]);
+ } else {
+ el.value = data[field];
+ }
}
}
} // else nothing
<%$pre%>tbody.removeChild(r);
}
+function <%$pre%>set_prefix(obj) {
+ if ( obj.id ) {
+ obj.id = <%$pre |js_string%> + obj.id;
+ }
+ if ( obj.getAttribute('name') ) {
+ obj.setAttribute('name', <%$pre |js_string%> + obj.getAttribute('name'));
+ }
+ for (var i = 0; i < obj.children.length; i++) {
+ if ( obj.children[i] instanceof Node ) {
+ <%$pre%>set_prefix(obj.children[i]);
+ }
+ }
+}
+
function <%$pre%>init() {
<%$pre%>template = document.getElementById(<% $template_row |js_string%>);
<%$pre%>tbody = document.getElementById('<%$pre%>autotable');
var table = <%$pre%>template.parentNode;
table.removeChild(<%$pre%>template);
// give it an id
- <%$pre%>template.id = <%$pre |js_string%> + 'row';
- // and a magic identifier so we know it's been submitted
+ <%$pre%>template.id = 'row';
+ // prefix the ids and names of the TR object and all its descendants
+ <%$pre%>set_prefix(<%$pre%>template);
+ // add a magic identifier so we know it's been submitted
var magic = document.createElement('INPUT');
magic.setAttribute('type', 'hidden');
magic.setAttribute('name', '<%$pre%>magic');
// and a delete button
%# should this be enclosed in an actual <button> for aesthetics?
var delete_button = document.createElement('IMG');
- delete_button.id = 'delete_button';
+ delete_button.id = '<%$pre%>delete_button';
delete_button.src = '<%$fsurl%>images/cross.png';
delete_button.alt = 'X';
// use an inline string for this so that it will be cloned properly
delete_button.setAttribute('onclick', "<%$pre%>deleteRow(this.rownum);");
+ // and an error display
+ var error_span = document.createElement('SPAN');
+ error_span.className = 'error';
+ error_span.style.color = '#FF0000';
+ error_span.setAttribute('name', '<%$pre%>error');
+ error_span.style.padding = '5px';
var delete_cell = document.createElement('TD');
+ delete_cell.style.textAlign = 'left';
delete_cell.appendChild(delete_button);
delete_cell.appendChild(magic); // it has to go somewhere
+ delete_cell.appendChild(error_span);
<%$pre%>template.appendChild(delete_cell);
// preload rows
border-spacing: 0;
width: 100%;
}
+table.usage {
+ border: 1px solid black;
+ margin: auto;
+ width: 60%;
+ border-spacing: 0px;
+}
+.shared > * {
+ background-color: #ffffaa;
+}
.row0 { background-color: #eeeeee; }
.row1 { background-color: #ffffff; }
% }
% }
</TABLE>
+% if ( @cust_pkg_usage ) {
+ <TABLE CLASS="usage inv">
+ <TR><TH COLSPAN=4><% mt('Included usage') %></TH></TR>
+% foreach my $usage (@cust_pkg_usage) {
+% my $part = $usage->part_pkg_usage;
+% my $ratio = 255 * ($usage->minutes / $part->minutes);
+% $ratio = 255 if $ratio > 255; # because rollover
+% my $color = sprintf('STYLE="font-weight: bold; color: #%02x%02x00"', 255 - $ratio, $ratio);
+% my $trstyle = '';
+% $trstyle = ' CLASS="shared"' if $part->shared;
+ <TR<%$trstyle%>>
+ <TD ALIGN="right"><% $part->description %>: </TD>
+ <TD <%$color%> ALIGN="right"><% $usage->minutes %></TD>
+ <TD <%$color%>> / </TD>
+ <TD <%$color%>><% $part->minutes %></TD>
+% if ( $part->shared ) {
+ <TD><I>(shared)</I></TD>
+% }
+ </TR>
+% }
+ </TABLE>
+% }
</TD>
|| ($countrydefault eq 'US' ? 'CA' : '');
my $supplemental = $opt{'supplemental'} || 0;
+
+$cust_pkg->pkgnum =~ /^(\d+)$/;
+my $pkgnum = $1;
+my @cust_pkg_usage = qsearch({
+ 'select' => 'cust_pkg_usage.*',
+ 'table' => 'cust_pkg_usage',
+ 'addl_from' => ' JOIN part_pkg_usage USING (pkgusagepart)',
+ 'extra_sql' => " WHERE pkgnum = $1",
+ 'order_by' => ' ORDER BY priority ASC, description ASC',
+});
+
#subroutines
#false laziness w/status.html