TT#35662 move product_id to billing.contracts

Change-Id: I5381b36e226b1c0c2032fa40c3a082b85485e4ac
(cherry picked from commit b35b0e1cbc)
(cherry picked from commit 2492ef2767)
(cherry picked from commit b7e82fdf1e)
changes/90/21190/2
Rene Krenn 7 years ago
parent 90dfbb534d
commit a56100b30f

@ -12,11 +12,14 @@ use HTTP::Status qw(:constants);
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages qw();
require Catalyst::ActionRole::ACL;
require Catalyst::ActionRole::CheckTrailingSlash;
require NGCP::Panel::Role::HTTPMethods;
require Catalyst::ActionRole::RequireSSL;
use NGCP::Panel::Utils::BillingMappings qw();
sub allowed_methods{
return [qw/GET POST OPTIONS HEAD/];
}
@ -203,7 +206,7 @@ sub POST :Allow {
}
my $mappings_to_create = [];
last unless NGCP::Panel::Utils::Contract::prepare_billing_mappings(
last unless NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings(
c => $c,
resource => $resource,
old_resource => undef,
@ -215,11 +218,12 @@ sub POST :Allow {
});
my $product_class = delete $resource->{type};
my $product = $schema->resultset('products')->find({ class => $product_class });
my $product = $schema->resultset('products')->search_rs({ class => $product_class })->first;
unless($product) {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'type'.");
last;
}
$resource->{product_id} = $product->id;
my $now = NGCP::Panel::Utils::DateTime::current_local;
$resource->{create_timestamp} = $now;

@ -12,6 +12,7 @@ use HTTP::Status qw(:constants);
use NGCP::Panel::Utils::ValidateJSON qw();
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Contract qw();
use NGCP::Panel::Utils::BillingMappings qw();
require Catalyst::ActionRole::ACL;
require NGCP::Panel::Role::HTTPMethods;
require Catalyst::ActionRole::RequireSSL;
@ -123,7 +124,7 @@ sub PATCH :Allow {
last unless $preference;
my $json = $self->get_valid_patch_data(
c => $c,
c => $c,
id => $id,
media_type => 'application/json-patch+json',
);
@ -132,9 +133,10 @@ sub PATCH :Allow {
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $contract = $self->contract_by_id($c, $id, $now);
last unless $self->resource_exists($c, contract => $contract);
my $old_resource = { $contract->get_inflated_columns };
my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $contract, );
#my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
$old_resource->{billing_profile_id} = $billing_mapping->billing_profile_id;
$old_resource->{billing_profile_definition} = undef;
delete $old_resource->{profile_package_id};
@ -142,7 +144,7 @@ sub PATCH :Allow {
my $resource = $self->apply_patch($c, $old_resource, $json, sub {
my ($missing_field,$entity) = @_;
if ($missing_field eq 'billing_profiles') {
$entity->{billing_profiles} = NGCP::Panel::Utils::Contract::resource_from_future_mappings($contract);
$entity->{billing_profiles} = NGCP::Panel::Utils::BillingMappings::resource_from_future_mappings($contract);
$entity->{billing_profile_definition} //= 'profiles';
}
});
@ -154,7 +156,7 @@ sub PATCH :Allow {
my $hal = $self->hal_from_contract($c, $contract, $form, $now);
last unless $self->add_update_journal_item_hal($c,$hal);
$guard->commit;
if ('minimal' eq $preference) {
@ -192,13 +194,14 @@ sub PUT :Allow {
);
last unless $resource;
my $old_resource = { $contract->get_inflated_columns };
my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $contract, );
#my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
$old_resource->{type} = $billing_mapping->product->class;
my $form = $self->get_form($c);
$contract = $self->update_contract($c, $contract, $old_resource, $resource, $form, $now);
last unless $contract;
my $hal = $self->hal_from_contract($c, $contract, $form, $now);
last unless $self->add_update_journal_item_hal($c,$hal);

@ -12,11 +12,14 @@ use HTTP::Status qw(:constants);
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages qw();
require Catalyst::ActionRole::ACL;
require Catalyst::ActionRole::CheckTrailingSlash;
require NGCP::Panel::Role::HTTPMethods;
require Catalyst::ActionRole::RequireSSL;
use NGCP::Panel::Utils::BillingMappings qw();
sub allowed_methods{
return [qw/GET POST OPTIONS HEAD/];
}
@ -276,7 +279,7 @@ sub POST :Allow {
}
my $mappings_to_create = [];
last unless NGCP::Panel::Utils::Contract::prepare_billing_mappings(
last unless NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings(
c => $c,
resource => $resource,
old_resource => undef,
@ -288,11 +291,12 @@ sub POST :Allow {
});
my $product_class = delete $resource->{type};
my $product = $schema->resultset('products')->find({ class => $product_class });
my $product = $schema->resultset('products')->search_rs({ class => $product_class })->first;
unless($product) {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'type'.");
last;
}
$resource->{product_id} = $product->id;
my $now = NGCP::Panel::Utils::DateTime::current_local;
$resource->{create_timestamp} = $now;
@ -301,6 +305,7 @@ sub POST :Allow {
try {
$customer = $schema->resultset('contracts')->create($resource);
$c->log->debug("customer id " . $customer->id . " created");
} catch($e) {
$c->log->error("failed to create customer contract: $e"); # TODO: user, message, trace, ...
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create customer.");

@ -12,6 +12,7 @@ use HTTP::Status qw(:constants);
use NGCP::Panel::Utils::ValidateJSON qw();
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Contract qw();
use NGCP::Panel::Utils::BillingMappings qw();
require Catalyst::ActionRole::ACL;
require NGCP::Panel::Role::HTTPMethods;
require Catalyst::ActionRole::RequireSSL;
@ -141,14 +142,15 @@ sub PATCH :Allow {
my $old_resource = { $customer->get_inflated_columns };
delete $old_resource->{profile_package_id};
my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $customer, );
#my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
$old_resource->{billing_profile_id} = $billing_mapping->billing_profile_id;
$old_resource->{billing_profile_definition} = undef;
my $resource = $self->apply_patch($c, $old_resource, $json, sub {
my ($missing_field,$entity) = @_;
if ($missing_field eq 'billing_profiles') {
$entity->{billing_profiles} = NGCP::Panel::Utils::Contract::resource_from_future_mappings($customer);
$entity->{billing_profiles} = NGCP::Panel::Utils::BillingMappings::resource_from_future_mappings($customer);
$entity->{billing_profile_definition} //= 'profiles';
} elsif ($missing_field eq 'profile_package_id') {
$entity->{profile_package_id} = $customer->profile_package_id;

@ -175,7 +175,7 @@ sub process_edit :Private {
profile_id => $c->stash->{profile_result}->id,
old_prepaid => $old_prepaid,
new_prepaid => $c->stash->{profile_result}->prepaid,
contract_rs => NGCP::Panel::Utils::Contract::get_contract_rs(schema => $schema),
contract_rs => NGCP::Panel::Utils::Contract::get_contract_rs(schema => $schema), #ok
);
});

@ -11,6 +11,7 @@ use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages;
use NGCP::Panel::Utils::Subscriber;
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::BillingMappings qw();
sub auto :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) {
my ($self, $c) = @_;
@ -22,16 +23,17 @@ sub auto :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) {
sub contract_list :Chained('/') :PathPart('contract') :CaptureArgs(0) {
my ($self, $c) = @_;
my $now = NGCP::Panel::Utils::DateTime::current_local;
$c->stash->{contract_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", search => 1, title => $c->loc("#") },
{ name => "external_id", search => 1, title => $c->loc("External #") },
{ name => "contact.email", search => 1, title => $c->loc("Contact Email") },
{ name => "billing_mappings_actual.billing_mappings.product.name", search => 1, title => $c->loc("Product") },
{ name => "billing_mappings_actual.billing_mappings.billing_profile.name", search => 1, title => $c->loc("Billing Profile") },
{ name => "product.name", search => 1, title => $c->loc("Product") },
{ name => 'billing_profile_name', accessor => "billing_profile_name", search => 0, title => $c->loc('Billing Profile'),
literal_sql => NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping_stmt(c => $c, now => $now, projection => 'billing_profile.name' ) },
{ name => "status", search => 1, title => $c->loc("Status") },
]);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $rs = NGCP::Panel::Utils::Contract::get_contract_rs(
schema => $c->model('DB'),
now => $now
@ -81,17 +83,11 @@ sub base :Chained('contract_list') :PathPart('') :CaptureArgs(1) {
my $contract_rs = $c->stash->{contract_select_rs}
->search({
'me.id' => $contract_id,
},{
'+select' => 'billing_mappings.id',
'+as' => 'bmid',
});
},undef);
my $contract_terminated_rs = $c->stash->{contract_select_all_rs}
->search({
'me.id' => $contract_id,
},{
'+select' => 'billing_mappings.id',
'+as' => 'bmid',
});
},undef);
my $contract_first = $contract_rs->first;
@ -104,16 +100,16 @@ sub base :Chained('contract_list') :PathPart('') :CaptureArgs(1) {
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/contract'));
}
my $billing_mapping = $contract_first->billing_mappings->find($contract_first->get_column('bmid'));
my $now = $c->stash->{now};
my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $contract_first, now => $now, );
if (! defined ($billing_mapping->product) || (
$billing_mapping->product->handle ne 'VOIP_RESELLER' &&
$billing_mapping->product->handle ne 'SIP_PEERING' &&
$billing_mapping->product->handle ne 'PSTN_PEERING')) {
}
my $now = $c->stash->{now};
my $billing_mappings_ordered = NGCP::Panel::Utils::Contract::billing_mappings_ordered($contract_rs->first->billing_mappings,$now,$contract_first->get_column('bmid'));
my $future_billing_mappings = NGCP::Panel::Utils::Contract::billing_mappings_ordered(NGCP::Panel::Utils::Contract::future_billing_mappings($contract_rs->first->billing_mappings,$now));
my $billing_mappings_ordered = NGCP::Panel::Utils::BillingMappings::billing_mappings_ordered($contract_first->billing_mappings,$now,$billing_mapping->id);
my $future_billing_mappings = NGCP::Panel::Utils::BillingMappings::billing_mappings_ordered(NGCP::Panel::Utils::BillingMappings::future_billing_mappings($contract_first->billing_mappings,$now));
$c->stash(contract => $contract_first);
$c->stash(contract_rs => $contract_rs);
@ -144,8 +140,8 @@ sub edit :Chained('base') :PathPart('edit') :Args(0) {
}
$params = merge($params, $c->session->{created_objects});
my ($form, $is_peering_reseller);
if (defined $billing_mapping->product &&
grep {$billing_mapping->product->handle eq $_}
if (defined $contract->product &&
grep {$contract->product->handle eq $_}
("SIP_PEERING", "PSTN_PEERING", "VOIP_RESELLER") ) {
$form = NGCP::Panel::Form::get("NGCP::Panel::Form::Contract::PeeringReseller", $c);
$is_peering_reseller = 1;
@ -186,7 +182,7 @@ sub edit :Chained('base') :PathPart('edit') :Args(0) {
my $mappings_to_create = [];
my $delete_mappings = 0;
my $set_package = ($form->values->{billing_profile_definition} // 'id') eq 'package';
NGCP::Panel::Utils::Contract::prepare_billing_mappings(
NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings(
c => $c,
resource => $form->values,
old_resource => { $contract->get_inflated_columns },
@ -202,11 +198,11 @@ sub edit :Chained('base') :PathPart('edit') :Args(0) {
my $old_package = $contract->profile_package;
$contract->update($form->values);
NGCP::Panel::Utils::Contract::remove_future_billing_mappings($contract,$now) if $delete_mappings;
NGCP::Panel::Utils::BillingMappings::remove_future_billing_mappings($contract,$now) if $delete_mappings;
foreach my $mapping (@$mappings_to_create) {
$contract->billing_mappings->create($mapping);
}
$contract = $c->stash->{contract_terminated_rs}->first;
#$contract = $c->stash->{contract_terminated_rs}->first;
my $balance = NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
contract => $contract,
@ -316,7 +312,7 @@ sub billingmappings_ajax :Chained('base') :PathPart('billingmappings/ajax') :Arg
my ($self, $c) = @_;
$c->stash(timeline_data => {
contract => { $c->stash->{contract}->get_columns },
events => NGCP::Panel::Utils::Contract::get_billingmappings_timeline_data($c,$c->stash->{contract}),
events => NGCP::Panel::Utils::BillingMappings::get_billingmappings_timeline_data($c,$c->stash->{contract}),
});
$c->detach( $c->view("JSON") );
}
@ -354,6 +350,7 @@ sub peering_create :Chained('peering_list') :PathPart('create') :Args(0) {
delete $params->{contact};
}
$c->stash->{type} = 'sippeering';
$c->stash(ajax_uri => $c->uri_for_action("/contract/ajax"));
my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::Contract::PeeringReseller", $c);
$form->process(
posted => $posted,
@ -378,9 +375,10 @@ sub peering_create :Chained('peering_list') :PathPart('create') :Args(0) {
}
$form->values->{external_id} = $form->field('external_id')->value;
$form->values->{create_timestamp} = $form->values->{modify_timestamp} = NGCP::Panel::Utils::DateTime::current_local;
$form->values->{product_id} = $schema->resultset('products')->search_rs({ class => $c->stash->{type} })->first->id;
my $mappings_to_create = [];
NGCP::Panel::Utils::Contract::prepare_billing_mappings(
NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings(
c => $c,
resource => $form->values,
mappings_to_create => $mappings_to_create,
@ -393,13 +391,10 @@ sub peering_create :Chained('peering_list') :PathPart('create') :Args(0) {
foreach my $mapping (@$mappings_to_create) {
$contract->billing_mappings->create($mapping);
}
$contract = $c->stash->{contract_select_rs}
->search({
'me.id' => $contract->id,
},{
'+select' => 'billing_mappings.id',
'+as' => 'bmid',
})->first;
#$contract = $c->stash->{contract_select_rs}
# ->search({
# 'me.id' => $contract->id,
# },undef)->first;
NGCP::Panel::Utils::ProfilePackages::create_initial_contract_balances(c => $c,
contract => $contract,
@ -478,16 +473,15 @@ sub reseller_ajax_contract_filter :Chained('reseller_list') :PathPart('ajax/cont
my $now = $c->stash->{now};
my $rs = NGCP::Panel::Utils::Contract::get_contract_rs(
schema => $c->model('DB'),
now => $now,
contract_id => $contract_id )
->search_rs({
now => $now)->search_rs({
'me.id' => $contract_id,
});
my $contract_columns = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", search => 1, title => $c->loc("#") },
{ name => "external_id", search => 1, title => $c->loc("External #") },
{ name => "contact.email", search => 1, title => $c->loc("Contact Email") },
{ name => "billing_mappings_actual.billing_mappings.billing_profile.name", search => 1, title => $c->loc("Billing Profile") },
{ name => 'billing_profile_name', accessor => "billing_profile_name", search => 0, title => $c->loc('Billing Profile'),
literal_sql => NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping_stmt(c => $c, now => $now, projection => 'billing_profile.name' ) },
{ name => "status", search => 1, title => $c->loc("Status") },
]);
NGCP::Panel::Utils::Datatables::process($c, $rs, $contract_columns);
@ -504,6 +498,7 @@ sub reseller_create :Chained('reseller_list') :PathPart('create') :Args(0) {
delete $params->{contact};
}
$c->stash->{type} = 'reseller';
$c->stash(ajax_uri => $c->uri_for_action("/contract/ajax"));
my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::Contract::PeeringReseller", $c);
$form->process(
posted => $posted,
@ -529,9 +524,10 @@ sub reseller_create :Chained('reseller_list') :PathPart('create') :Args(0) {
}
$form->values->{external_id} = $form->field('external_id')->value;
$form->values->{create_timestamp} = $form->values->{modify_timestamp} = NGCP::Panel::Utils::DateTime::current_local;
$form->values->{product_id} = $schema->resultset('products')->search_rs({ class => $c->stash->{type} })->first->id;
my $mappings_to_create = [];
NGCP::Panel::Utils::Contract::prepare_billing_mappings(
NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings(
c => $c,
resource => $form->values,
mappings_to_create => $mappings_to_create,
@ -544,13 +540,10 @@ sub reseller_create :Chained('reseller_list') :PathPart('create') :Args(0) {
foreach my $mapping (@$mappings_to_create) {
$contract->billing_mappings->create($mapping);
}
$contract = $c->stash->{contract_select_rs}
->search({
'me.id' => $contract->id,
},{
'+select' => 'billing_mappings.id',
'+as' => 'bmid',
})->first;
#$contract = $c->stash->{contract_select_rs}
# ->search({
# 'me.id' => $contract->id,
# },undef)->first;
NGCP::Panel::Utils::ProfilePackages::create_initial_contract_balances(c => $c,
contract => $contract,

@ -13,6 +13,7 @@ use NGCP::Panel::Utils::Subscriber;
use NGCP::Panel::Utils::Sounds;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages;
use NGCP::Panel::Utils::BillingMappings qw();
use NGCP::Panel::Utils::DeviceBootstrap;
use NGCP::Panel::Utils::Voucher;
use NGCP::Panel::Utils::ContractLocations qw();
@ -41,6 +42,7 @@ sub auto :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRol
sub list_customer :Chained('/') :PathPart('customer') :CaptureArgs(0) {
my ($self, $c) = @_;
my $now = NGCP::Panel::Utils::DateTime::current_local;
$c->stash->{contract_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", search => 1, title => $c->loc("#") },
{ name => "external_id", search => 1, title => $c->loc("External #") },
@ -49,17 +51,17 @@ sub list_customer :Chained('/') :PathPart('customer') :CaptureArgs(0) {
{ name => "contact.firstname", search => 1, title => '' },
{ name => "contact.lastname", search => 1, title => $c->loc("Name"),
custom_renderer => 'function ( data, type, full ) { var sep = (full.contact_firstname && full.contact_lastname) ? " " : ""; return (full.contact_firstname || "") + sep + (full.contact_lastname || ""); }' },
{ name => "billing_mappings_actual.billing_mappings.product.name", search => 1, title => $c->loc("Product") },
{ name => "billing_mappings_actual.billing_mappings.billing_profile.name", search => 1, title => $c->loc("Billing Profile") },
{ name => "product.name", search => 1, title => $c->loc("Product") },
{ name => 'billing_profile_name', accessor => "billing_profile_name", search => 0, title => $c->loc('Billing Profile'),
literal_sql => '""' },
{ name => "status", search => 1, title => $c->loc("Status") },
{ name => "max_subscribers", search => 1, title => $c->loc("Max. Subscribers") },
]);
my $now = NGCP::Panel::Utils::DateTime::current_local;
my $rs = NGCP::Panel::Utils::Contract::get_customer_rs(c => $c, now => $now);
my $rs = NGCP::Panel::Utils::Contract::get_customer_rs(c => $c); #, now => $now);
my $rs_all = NGCP::Panel::Utils::Contract::get_contract_rs(
schema => $c->model('DB'),
now => $now,
#now => $now,
include_terminated => 1,
);
$c->stash(
@ -76,11 +78,14 @@ sub root :Chained('list_customer') :PathPart('') :Args(0) :Does(ACL) :ACLDetachT
sub ajax :Chained('list_customer') :PathPart('ajax') :Args(0) {
my ($self, $c) = @_;
my $now = NGCP::Panel::Utils::DateTime::current_local; #uniform ts
my $res = $c->stash->{contract_select_rs};
NGCP::Panel::Utils::Datatables::process($c, $res, $c->stash->{contract_dt_columns}, sub {
my $item = shift;
my %contact = $item->contact->get_inflated_columns;
my %result = map { (ref $contact{$_}) ? () : ('contact_'.$_ => $contact{$_}); } keys %contact;
my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $item, now => $now, );
$result{'billing_profile_name'} = $bm_actual->billing_profile->name if $bm_actual;
return %result;
},);
$c->detach( $c->view("JSON") );
@ -107,7 +112,7 @@ sub ajax_reseller_filter :Chained('list_customer') :PathPart('ajax/reseller') :A
my $reseller_customer_columns = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", search => 1, title => $c->loc("#") },
{ name => "external_id", search => 1, title => $c->loc("External #") },
{ name => "billing_mappings_actual.billing_mappings.product.name", search => 1, title => $c->loc("Product") },
{ name => "product.name", search => 1, title => $c->loc("Product") },
{ name => "contact.email", search => 1, title => $c->loc("Contact Email") },
{ name => "status", search => 1, title => $c->loc("Status") },
]);
@ -140,11 +145,14 @@ sub ajax_package_filter :Chained('list_customer') :PathPart('ajax/package') :Arg
sub ajax_pbx_only :Chained('list_customer') :PathPart('ajax_pbx_only') :Args(0) {
my ($self, $c) = @_;
my $now = NGCP::Panel::Utils::DateTime::current_local; #uniform ts
my $res = $c->stash->{contract_select_rs}->search_rs({'product.class' => 'pbxaccount'});
NGCP::Panel::Utils::Datatables::process($c, $res, $c->stash->{contract_dt_columns}, sub {
my $item = shift;
my %contact = $item->contact->get_inflated_columns;
my %result = map { (ref $contact{$_}) ? () : ('contact_'.$_ => $contact{$_}); } keys %contact;
my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $item, now => $now, );
$result{'billing_profile_name'} = $bm_actual->billing_profile->name if $bm_actual;
return %result;
},);
$c->detach( $c->view("JSON") );
@ -201,12 +209,13 @@ sub create :Chained('list_customer') :PathPart('create') :Args(0) {
#$form->values->{profile_package_id} = undef unless NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES;
$form->values->{create_timestamp} = $form->values->{modify_timestamp} = NGCP::Panel::Utils::DateTime::current_local;
$form->values->{external_id} = $form->field('external_id')->value;
$form->values->{product_id} //= $schema->resultset('products')->search_rs({ class => $c->stash->{type} })->first->id;
unless($form->values->{max_subscribers} && length($form->values->{max_subscribers})) {
delete $form->values->{max_subscribers};
}
my $mappings_to_create = [];
NGCP::Panel::Utils::Contract::prepare_billing_mappings(
NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings(
c => $c,
resource => $form->values,
mappings_to_create => $mappings_to_create,
@ -214,19 +223,16 @@ sub create :Chained('list_customer') :PathPart('create') :Args(0) {
my ($err,@fields) = @_;
die( [$err, "showdetails"] );
});
delete $form->values->{product_id};
#delete $form->values->{product_id};
my $contract = $schema->resultset('contracts')->create($form->values);
foreach my $mapping (@$mappings_to_create) {
$contract->billing_mappings->create($mapping);
}
$contract = $c->stash->{contract_select_rs}
->search({
'me.id' => $contract->id,
},{
'+select' => 'billing_mappings.id',
'+as' => 'bmid',
})->first;
#$contract = $c->stash->{contract_select_rs}
# ->search({
# 'me.id' => $contract->id,
# },undef)->first;
NGCP::Panel::Utils::ProfilePackages::create_initial_contract_balances(c => $c,
contract => $contract,
@ -278,17 +284,11 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) {
my $contract_rs = $c->stash->{contract_select_rs}
->search({
'me.id' => $contract_id,
},{
'+select' => 'billing_mappings.id',
'+as' => 'bmid',
});
},undef);
my $contract_terminated_rs = $c->stash->{contract_select_all_rs}
->search({
'me.id' => $contract_id,
},{
'+select' => 'billing_mappings.id',
'+as' => 'bmid',
});
},undef);
if($c->user->roles eq 'reseller') {
$contract_rs = $contract_rs->search({
@ -305,7 +305,8 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) {
$c->detach('/denied_page');
}
}
unless(defined($contract_rs->first)) {
my $contract_first = $contract_rs->first;
unless(defined($contract_first)) {
NGCP::Panel::Utils::Message::error(
c => $c,
log => 'Customer was not found',
@ -314,10 +315,11 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) {
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/customer'));
}
my $now = $c->stash->{now};
my $billing_mappings_ordered = NGCP::Panel::Utils::Contract::billing_mappings_ordered($contract_rs->first->billing_mappings,$now,$contract_rs->first->get_column('bmid'));
my $future_billing_mappings = NGCP::Panel::Utils::Contract::billing_mappings_ordered(NGCP::Panel::Utils::Contract::future_billing_mappings($contract_rs->first->billing_mappings,$now));
my $billing_mapping = $contract_rs->first->billing_mappings->find($contract_rs->first->get_column('bmid'));
my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $contract_first, now => $now, );
my $billing_mappings_ordered = NGCP::Panel::Utils::BillingMappings::billing_mappings_ordered($contract_first->billing_mappings,$now,$billing_mapping->id);
my $future_billing_mappings = NGCP::Panel::Utils::BillingMappings::billing_mappings_ordered(NGCP::Panel::Utils::BillingMappings::future_billing_mappings($contract_first->billing_mappings,$now));
my $balance;
try {
@ -346,20 +348,20 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) {
NGCP::Panel::Utils::ProfilePackages::get_topuplog_datatable_cols($c),
]);
my $product_id = $contract_rs->first->get_column('product_id');
my $product = $contract_first->product;
NGCP::Panel::Utils::Message::error(
c => $c,
error => "No product for customer contract id $contract_id found",
desc => $c->loc('No product for this customer contract found.'),
) unless($product_id);
my $product = $c->model('DB')->resultset('products')->find($product_id);
NGCP::Panel::Utils::Message::error(
c => $c,
error => "No product with id $product_id for customer contract id $contract_id found",
desc => $c->loc('Invalid product id for this customer contract.'),
) unless($product);
#my $product = $c->model('DB')->resultset('products')->find($product_id);
#NGCP::Panel::Utils::Message::error(
# c => $c,
# error => "No product with id $product_id for customer contract id $contract_id found",
# desc => $c->loc('Invalid product id for this customer contract.'),
#) unless($product);
# only show the extension if it's a pbx extension. otherwise (and in case of a pilot?) show the
# number
@ -407,7 +409,6 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) {
})->all ];
# contents of details page:
my $contract_first = $contract_rs->first;
NGCP::Panel::Utils::Sounds::stash_soundset_list(c => $c, contract => $contract_first);
$c->stash->{contact_hash} = { $contract_first->contact->get_inflated_columns };
if(defined $contract_first->max_subscribers) {
@ -534,7 +535,7 @@ sub edit :Chained('base_restricted') :PathPart('edit') :Args(0) {
my $mappings_to_create = [];
my $delete_mappings = 0;
my $set_package = ($form->values->{billing_profile_definition} // 'id') eq 'package';
NGCP::Panel::Utils::Contract::prepare_billing_mappings(
NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings(
c => $c,
resource => $form->values,
old_resource => { $contract->get_inflated_columns },
@ -545,7 +546,7 @@ sub edit :Chained('base_restricted') :PathPart('edit') :Args(0) {
my ($err,@fields) = @_;
die( [$err, "showdetails"] );
});
delete $form->values->{product_id};
#delete $form->values->{product_id};
my $old_prepaid = $billing_mapping->billing_profile->prepaid;
my $old_ext_id = $contract->external_id // '';
@ -553,11 +554,11 @@ sub edit :Chained('base_restricted') :PathPart('edit') :Args(0) {
my $old_package = $contract->profile_package;
$contract->update($form->values);
NGCP::Panel::Utils::Contract::remove_future_billing_mappings($contract,$now) if $delete_mappings;
NGCP::Panel::Utils::BillingMappings::remove_future_billing_mappings($contract,$now) if $delete_mappings;
foreach my $mapping (@$mappings_to_create) {
$contract->billing_mappings->create($mapping);
}
$contract = $c->stash->{contract_terminated_rs}->first;
#$contract = $c->stash->{contract_terminated_rs}->first;
my $balance = NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
contract => $contract,
@ -571,7 +572,7 @@ sub edit :Chained('base_restricted') :PathPart('edit') :Args(0) {
profiles_added => ($set_package ? scalar @$mappings_to_create : 0),
);
$billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
$billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(contract => $contract, schema => $schema, now => $now, );
$billing_profile = $billing_mapping->billing_profile;
my $new_ext_id = $contract->external_id // '';
@ -1257,7 +1258,7 @@ sub billingmappings_ajax :Chained('base') :PathPart('billingmappings/ajax') :Arg
my ($self, $c) = @_;
$c->stash(timeline_data => {
contract => { $c->stash->{contract}->get_columns },
events => NGCP::Panel::Utils::Contract::get_billingmappings_timeline_data($c,$c->stash->{contract}),
events => NGCP::Panel::Utils::BillingMappings::get_billingmappings_timeline_data($c,$c->stash->{contract}),
});
$c->detach( $c->view("JSON") );
}

@ -14,6 +14,7 @@ use NGCP::Panel::Utils::Navigation;
use NGCP::Panel::Utils::Reseller;
use NGCP::Panel::Utils::BillingNetworks qw();
use NGCP::Panel::Utils::ProfilePackages qw();
use NGCP::Panel::Utils::BillingMappings qw();
use NGCP::Panel::Utils::Billing qw();
use NGCP::Panel::Utils::Admin;
@ -50,7 +51,8 @@ sub list_reseller :Chained('/') :PathPart('reseller') :CaptureArgs(0) {
{ name => "id", search => 1, title => $c->loc("#") },
{ name => "external_id", search => 1, title => $c->loc("External #") },
{ name => "contact.email", search => 1, title => $c->loc("Contact Email") },
{ name => "billing_mappings_actual.billing_mappings.billing_profile.name", search => 1, title => $c->loc("Billing Profile") },
{ name => 'billing_profile_name', accessor => "billing_profile_name", search => 0, title => $c->loc('Billing Profile'),
literal_sql => NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping_stmt(c => $c, projection => 'billing_profile.name' ) },
{ name => "status", search => 1, title => $c->loc("Status") },
]);
}
@ -93,8 +95,8 @@ sub create :Chained('list_reseller') :PathPart('create') :Args(0) :Does(ACL) :AC
item => $params,
);
NGCP::Panel::Utils::Navigation::check_form_buttons(
c => $c,
form => $form,
c => $c,
form => $form,
fields => {'contract.create' => $c->uri_for('/contract/reseller/create') },
back_uri => $c->req->uri,
);
@ -162,7 +164,7 @@ sub base :Chained('list_reseller') :PathPart('') :CaptureArgs(1) {
{ name => "firstname", search => 1, title => $c->loc('First Name') },
{ name => "lastname", search => 1, title => $c->loc('Last Name') },
{ name => "company", search => 1, title => $c->loc('Company') },
{ name => "email", search => 1, title => $c->loc('Email') },
{ name => "email", search => 1, title => $c->loc('Email') },
]);
$c->stash->{reseller_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", search => 1, title => $c->loc('#') },
@ -196,29 +198,29 @@ sub base :Chained('list_reseller') :PathPart('') :CaptureArgs(1) {
{ name => 'name', search => 1, title => $c->loc('Name') },
{ name => 'type', search => 1, title => $c->loc('Type') },
]);
$c->stash->{profile_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => "id", "search" => 1, "title" => $c->loc("#") },
{ name => "name", "search" => 1, "title" => $c->loc("Name") },
#{ name => "reseller.name", "search" => 1, "title" => $c->loc("Reseller") },
#{ name => "v_count_used", "search" => 0, "title" => $c->loc("Used") },
NGCP::Panel::Utils::Billing::get_datatable_cols($c),
]);
]);
$c->stash->{network_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => 'id', search => 1, title => $c->loc('#') },
#{ name => 'reseller.name', search => 1, title => $c->loc('Reseller') },
{ name => 'name', search => 1, title => $c->loc('Name') },
NGCP::Panel::Utils::BillingNetworks::get_datatable_cols($c),
]);
]);
$c->stash->{package_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [
{ name => 'id', search => 1, title => $c->loc('#') },
#{ name => 'reseller.name', search => 1, title => $c->loc('Reseller') },
{ name => 'name', search => 1, title => $c->loc('Name') },
NGCP::Panel::Utils::ProfilePackages::get_datatable_cols($c),
]);
$c->stash(reseller => $c->stash->{resellers}->search_rs({ id => $reseller_id }));
unless($c->stash->{reseller}->first) {
NGCP::Panel::Utils::Message::error(
@ -406,20 +408,18 @@ sub details :Chained('base') :PathPart('details') :Args(0) :Does(ACL) :ACLDetach
sub ajax_contract :Chained('list_reseller') :PathPart('ajax_contract') :Args(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) {
my ($self, $c) = @_;
my $edit_contract_id = $c->session->{edit_contract_id};
my @used_contracts = map {
unless($edit_contract_id && $edit_contract_id == $_->get_column('contract_id')) {
$_->get_column('contract_id')
} else {}
} $c->stash->{resellers}->all;
my $free_contracts = NGCP::Panel::Utils::Contract::get_contract_rs(
schema => $c->model('DB'))
->search_rs({
'me.status' => { '!=' => 'terminated' },
'me.id' => { 'not in' => \@used_contracts },
'product.class' => 'reseller',
});
my $free_contracts = NGCP::Panel::Utils::Contract::get_contract_rs(schema => $c->model('DB'))->search_rs({
'me.status' => { '!=' => 'terminated' },
'me.id' => { 'not in' => \@used_contracts },
'product.class' => 'reseller',
});
NGCP::Panel::Utils::Datatables::process($c, $free_contracts, $c->stash->{contract_dt_columns});
$c->detach( $c->view("JSON") );
}
@ -433,6 +433,7 @@ sub create_defaults :Path('create_defaults') :Args(0) :Does(ACL) :ACLDetachTo('/
my $default_pass = 'defaultresellerpassword';
my $saltedpass = NGCP::Panel::Utils::Admin::generate_salted_hash($default_pass);
my $billing = $c->model('DB');
my $now = NGCP::Panel::Utils::DateTime::current_local;
my %defaults = (
contacts => {
@ -445,6 +446,7 @@ sub create_defaults :Path('create_defaults') :Args(0) :Does(ACL) :ACLDetachTo('/
status => 'active',
create_timestamp => $now,
activate_timestamp => $now,
product_id => $billing->resultset('products')->search_rs({ class => 'reseller' })->first->id,
},
resellers => {
name => 'Default reseller' . sprintf('%04d', rand 10000),
@ -462,7 +464,6 @@ sub create_defaults :Path('create_defaults') :Args(0) :Does(ACL) :ACLDetachTo('/
);
$defaults{admins}->{login} = $defaults{resellers}->{name} =~ tr/A-Za-z0-9//cdr;
my $billing = $c->model('DB');
my %r;
try {
$billing->txn_do(sub {
@ -479,7 +480,8 @@ sub create_defaults :Path('create_defaults') :Args(0) :Does(ACL) :ACLDetachTo('/
my $resource = { $r{contracts}->get_inflated_columns };
$resource->{billing_profile_id} = 1;
$resource->{type} = 'reseller';
NGCP::Panel::Utils::Contract::prepare_billing_mappings(
NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings(
c => $c,
resource => $resource,
old_resource => undef,
@ -489,16 +491,11 @@ sub create_defaults :Path('create_defaults') :Args(0) :Does(ACL) :ACLDetachTo('/
die( [$err, "showdetails"] );
});
foreach my $mapping (@$mappings_to_create) {
$r{contracts}->billing_mappings->create($mapping);
$r{contracts}->billing_mappings->create($mapping);
}
$r{contracts} = NGCP::Panel::Utils::Contract::get_contract_rs(
schema => $c->model('DB'),
contract_id => $r{contracts}->id )->search(undef, {
'+select' => 'billing_mappings.id',
'+as' => 'bmid',
})->find($r{contracts}->id);
$r{contracts} = NGCP::Panel::Utils::Contract::get_contract_rs(schema => $c->model('DB'))->find($r{contracts}->id);
$r{billing_mappings} = $r{contracts}->billing_mappings;
$r{admins} = $billing->resultset('admins')->create({
%{ $defaults{admins} },
reseller_id => $r{resellers}->id,

@ -21,7 +21,7 @@ sub auto :Private {
NGCP::Panel::Utils::Navigation::check_redirect_chain(c => $c);
# only allow access to admin/reseller if cloudpbx is not enabled
if(!$c->config->{features}->{cloudpbx} &&
if(!$c->config->{features}->{cloudpbx} &&
$c->user->roles ne "admin" &&
$c->user->roles ne "reseller") {
@ -36,12 +36,10 @@ sub auto :Private {
# and then again, it's only for subscriberadmins with pbxaccount product
if($c->user->roles eq "subscriberadmin") {
my $contract_id = $c->user->account_id;
my $contract_select_rs = NGCP::Panel::Utils::Contract::get_contract_rs(
schema => $c->model('DB'),
contract_id => $contract_id,
);
$contract_select_rs = $contract_select_rs->search({ 'me.id' => $contract_id });
my $product_id = $contract_select_rs->first ? $contract_select_rs->first->get_column('product_id') : undef;
my $contract_select_rs = NGCP::Panel::Utils::Contract::get_contract_rs(schema => $c->model('DB'))->search_rs({
'me.id' => $contract_id,
});
my $product_id = $contract_select_rs->first ? $contract_select_rs->first->product->id : undef;
unless($product_id) {
NGCP::Panel::Utils::Message::error(
c => $c,
@ -50,9 +48,9 @@ sub auto :Private {
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/sound'));
}
my $product = $c->model('DB')->resultset('products')->find({
my $product = $c->model('DB')->resultset('products')->search_rs({
id => $product_id, class => 'pbxaccount',
});
})->first;
unless($product) {
$c->detach('/denied_page');
}
@ -67,7 +65,7 @@ sub sets_list :Chained('/') :PathPart('sound') :CaptureArgs(0) {
if($c->stash->{contract_rs}) {
NGCP::Panel::Utils::Sounds::stash_soundset_list(
c => $c,
c => $c,
contract => $c->stash->{contract_rs}->first,
);
} else {
@ -107,7 +105,7 @@ sub contract_sets_list :Chained('/') :PathPart('sound/contract') :CaptureArgs(1)
}
NGCP::Panel::Utils::Sounds::stash_soundset_list(
c => $c,
c => $c,
contract => $contract,
);
$c->stash(template => 'sound/list.tt');
@ -121,7 +119,7 @@ sub root :Chained('sets_list') :PathPart('') :Args(0) {
sub ajax :Chained('sets_list') :PathPart('ajax') :Args(0) {
my ($self, $c) = @_;
my $resultset = $c->stash->{sets_rs};
NGCP::Panel::Utils::Datatables::process($c, $resultset, $c->stash->{soundset_dt_columns});
$c->detach( $c->view("JSON") );
@ -130,7 +128,7 @@ sub ajax :Chained('sets_list') :PathPart('ajax') :Args(0) {
sub contract_ajax :Chained('contract_sets_list') :PathPart('ajax') :Args(0) {
my ($self, $c) = @_;
my $resultset = $c->stash->{sets_rs};
NGCP::Panel::Utils::Datatables::process($c, $resultset, $c->stash->{soundset_dt_columns});
$c->detach( $c->view("JSON") );
@ -229,7 +227,7 @@ sub edit :Chained('base') :PathPart('edit') {
my $old_contract_default = $c->stash->{set_result}->contract_default;
$c->stash->{set_result}->update($form->values);
if($c->stash->{set_result}->contract &&
if($c->stash->{set_result}->contract &&
$c->stash->{set_result}->contract_default == 1 && $old_contract_default != 1) {
# go over each subscriber in the contract and set the contract_sound_set
# preference if it doesn't have one set yet
@ -238,7 +236,7 @@ sub edit :Chained('base') :PathPart('edit') {
my $prov_subscriber = $bill_subscriber->provisioning_voip_subscriber;
if($prov_subscriber) {
my $pref_rs = NGCP::Panel::Utils::Preferences::get_usr_preference_rs(
c => $c, prov_subscriber => $prov_subscriber, attribute => 'contract_sound_set',
c => $c, prov_subscriber => $prov_subscriber, attribute => 'contract_sound_set',
);
unless($pref_rs->first) {
$pref_rs->create({ value => $c->stash->{set_result}->id });
@ -281,7 +279,7 @@ sub delete :Chained('base') :PathPart('delete') {
# remove all usr_preferenes where this set is assigned
if($c->stash->{set_result}->contract_id) {
my $pref_rs = NGCP::Panel::Utils::Preferences::get_usr_preference_rs(
c => $c, attribute => 'contract_sound_set',
c => $c, attribute => 'contract_sound_set',
);
$pref_rs->search({ value => $c->stash->{set_result}->id })->delete;
}
@ -403,7 +401,7 @@ sub create :Chained('sets_list') :PathPart('create') :Args() {
my $prov_subscriber = $bill_subscriber->provisioning_voip_subscriber;
if($prov_subscriber) {
my $pref_rs = NGCP::Panel::Utils::Preferences::get_usr_preference_rs(
c => $c, prov_subscriber => $prov_subscriber, attribute => 'contract_sound_set',
c => $c, prov_subscriber => $prov_subscriber, attribute => 'contract_sound_set',
);
unless($pref_rs->first) {
$pref_rs->create({ value => $set->id });
@ -437,13 +435,13 @@ sub create :Chained('sets_list') :PathPart('create') :Args() {
sub handles_list :Chained('base') :PathPart('handles') :CaptureArgs(0) {
my ( $self, $c ) = @_;
my $files_rs = $c->stash->{set_result}->voip_sound_files;
$c->stash(files_rs => $files_rs);
$c->stash(handles_base_uri =>
$c->uri_for_action("/sound/handles_root", [$c->req->captures->[0]]));
my $handles_rs = $c->model('DB')->resultset('voip_sound_groups')
->search({
},{
@ -464,8 +462,8 @@ sub handles_list :Chained('base') :PathPart('handles') :CaptureArgs(0) {
});
if($c->stash->{set_result}->contract_id) {
$handles_rs = $handles_rs->search({
'groups.name' => { '-in' => [qw/pbx music_on_hold digits/] }
$handles_rs = $handles_rs->search({
'groups.name' => { '-in' => [qw/pbx music_on_hold digits/] }
});
} else {
#$handles_rs = $handles_rs->search({ 'groups.name' => { '!=' => 'pbx' } });
@ -484,9 +482,9 @@ sub handles_list :Chained('base') :PathPart('handles') :CaptureArgs(0) {
$handles_rs = $handles_rs->search({ 'groups.name' => { '!=' => 'mobile_push' } });
}
my @rows = $handles_rs->all;
my %groups;
for my $handle (@rows) {
$groups{ $handle->get_column('groupname') } = []
@ -562,12 +560,12 @@ sub handles_edit :Chained('handles_base') :PathPart('edit') {
fields => {},
back_uri => $c->req->uri,
);
if($posted && $form->validated) {
if (defined $upload) {
my $soundfile = eval { $upload->slurp };
my $filename = eval { $upload->filename };
my $ft = File::Type->new();
unless ($ft->checktype_contents($soundfile) eq 'audio/x-wav') {
NGCP::Panel::Utils::Message::error(
@ -577,7 +575,7 @@ sub handles_edit :Chained('handles_base') :PathPart('edit') {
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->stash->{handles_base_uri});
}
my $target_codec = 'WAV';
# clear audio caches
@ -604,7 +602,7 @@ sub handles_edit :Chained('handles_base') :PathPart('edit') {
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->stash->{handles_base_uri});
}
try {
$file_result->update({
loopplay => $form->values->{loopplay},
@ -650,7 +648,7 @@ sub handles_edit :Chained('handles_base') :PathPart('edit') {
sub handles_delete :Chained('handles_base') :PathPart('delete') {
my ($self, $c) = @_;
try {
$c->stash->{file_result}->delete;
NGCP::Panel::Utils::Message::info(
@ -681,7 +679,7 @@ sub handles_delete :Chained('handles_base') :PathPart('delete') {
sub handles_download :Chained('handles_base') :PathPart('download') :Args(0) {
my ($self, $c) = @_;
my $file = $c->stash->{file_result};
my $filename = $file->filename;
$filename =~ s/\.\w+$/.wav/;
@ -702,7 +700,7 @@ sub handles_download :Chained('handles_base') :PathPart('download') :Args(0) {
} else {
$data = $file->data;
}
$c->response->header ('Content-Disposition' => 'attachment; filename="' . $filename . '"');
$c->response->content_type('audio/x-wav');
$c->response->body($data);
@ -725,7 +723,7 @@ sub handles_load_default :Chained('handles_list') :PathPart('loaddefault') :Args
fields => {},
back_uri => $c->req->uri,
);
if($posted && $form->validated) {
my $lang = $form->params->{language};
my $base = "/var/lib/ngcp-soundsets";

@ -141,7 +141,7 @@ sub base :Chained('sub_list') :PathPart('') :CaptureArgs(1) {
);
NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/subscriber'));
}
my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $contract );
$c->stash->{billing_mapping} = $billing_mapping;
$c->stash->{subscribers} = $c->model('DB')->resultset('voip_subscribers')->search({

@ -12,7 +12,7 @@ has_field 'id' => (
template => 'helpers/datatables_field.tt',
ajax_src => '/contract/peering/ajax',
table_titles => ['#', 'Status', 'Contact Email', 'Billing Profile'],
table_fields => ['id', 'status', 'contact_email', 'billing_mappings_actual_billing_mappings_billing_profile_name'],
table_fields => ['id', 'status', 'contact_email', 'billing_profile_name'],
);
has_field 'create' => (

@ -6,6 +6,7 @@ extends 'HTML::FormHandler';
use HTML::FormHandler::Widget::Block::Bootstrap;
use DateTime::Format::Strptime qw();
use Storable qw();
use NGCP::Panel::Utils::BillingMappings qw();
with 'NGCP::Panel::Render::RepeatableJs';
@ -16,7 +17,7 @@ sub build_form_element_class { [qw/form-horizontal/] }
has_field 'billing_profile_definition' => (
type => 'Select',
label => 'Set billing profiles',
label => 'Set billing profiles by',
options => [
{ value => 'id', label => 'single (actual billing mapping)' },
{ value => 'profiles', label => 'schedule (billing mapping intervals)' },
@ -265,7 +266,7 @@ sub validate {
my $old_resource = (exists $c->stash->{contract} ? { $c->stash->{contract}->get_inflated_columns } : undef);
my $mappings_to_create = [];
NGCP::Panel::Utils::Contract::prepare_billing_mappings(
NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings(
c => $c,
resource => $resource,
old_resource => $old_resource,

@ -33,7 +33,7 @@ sub validate_max_subscribers {
my $product = $form->field('product');
my $max_subscribers = $form->field('max_subscribers');
my $sipaccount = $c->model('DB')->resultset('products')->find({class => 'sipaccount'});
my $sipaccount = $c->model('DB')->resultset('products')->search_rs({class => 'sipaccount'})->first;
return unless $sipaccount;
my $sipaccount_id = $sipaccount->id // 0;

@ -78,6 +78,7 @@ sub get_valid_data{
}
return ($resource, $data);
}
sub get_valid_post_data {
@ -414,7 +415,7 @@ sub allowed_methods_filtered {
#
#old: allowed_roles = [qw/admin subscriber /]
#
#from now also possible:
#from now also possible:
#sub config_allowed_roles {
# return {
# 'Default' => [qw/admin reseller subscriberadmin/],
@ -610,7 +611,6 @@ sub paginate_order_collection_rs {
my ($self, $c, $item_rs, $params) = @_;
my($page,$rows,$order_by,$direction) = @$params{qw/page rows order_by direction/};
my $total_count;
my $no_count = $self->dont_count_collection_total($c);
if ( !$no_count ) {
@ -648,13 +648,13 @@ sub collection_nav_links {
$params //= $c->request->params;
delete @{$params}{'page', 'rows'};
my $rest_params = join( '&', map {"$_=".$params->{$_}} keys %{$params});
my $rest_params = join( '&', map {"$_=".(defined $params->{$_} ? $params->{$_} : '');} keys %{$params});
$rest_params = $rest_params ? "&$rest_params" : "";
my @links = (NGCP::Panel::Utils::DataHalLink->new(relation => 'self', href => sprintf('/%s?page=%s&rows=%s%s', $path, $page, $rows, $rest_params)));
if ( (! defined $total_count
&& ! $c->stash->{collection_infinite_pager_stop} )
if ( (! defined $total_count
&& ! $c->stash->{collection_infinite_pager_stop} )
|| ( defined $total_count && ($total_count / $rows) > $page ) ) {
push @links, NGCP::Panel::Utils::DataHalLink->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d%s', $path, $page + 1, $rows, $rest_params));
}

@ -81,6 +81,7 @@ sub hal_from_item {
$resource{id} = int($item->id);
$hal->resource({%resource});
return $hal;
}
sub item_by_id {

@ -63,8 +63,8 @@ sub hal_from_balance {
my $contract = $item->contract;
my $is_customer = (defined $contract->contact->reseller_id ? 1 : 0);
my $bm_start = NGCP::Panel::Utils::ProfilePackages::get_actual_billing_mapping(c => $c, contract => $contract, now => $item->start);
my $profile_at_start = $bm_start->billing_mappings->first->billing_profile;
my $bm_start = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $contract, now => $item->start);
my $profile_at_start = $bm_start->billing_profile;
my $is_actual = NGCP::Panel::Utils::DateTime::is_infinite_future($item->end) || NGCP::Panel::Utils::DateTime::set_local_tz($item->end) >= $now;
my ($is_timely,$timely_start,$timely_end) = NGCP::Panel::Utils::ProfilePackages::get_timely_range(
package => $contract->profile_package,

@ -166,7 +166,7 @@ sub update_profile {
profile_id => $profile->id,
old_prepaid => $old_prepaid,
new_prepaid => $profile->prepaid,
contract_rs => NGCP::Panel::Utils::Contract::get_contract_rs(schema => $c->model('DB')),
contract_rs => NGCP::Panel::Utils::Contract::get_contract_rs(schema => $c->model('DB')), #ok
);
} catch($e) {
$c->log->error("Failed to update billing profile '".$profile->id."': $e");

@ -85,7 +85,7 @@ sub _item_rs {
});
my $customer = $customer_rs->first;
my $cpbx = ($customer->get_column('product_class') eq 'pbxaccount') ? 1 : 0;
my $cpbx = ($customer->product->class eq 'pbxaccount') ? 1 : 0;
$cloudpbx &= $cpbx;
# TODO: sms and rtcengine are not specially restricted; should it?

@ -13,6 +13,7 @@ use HTTP::Status qw(:constants);
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages qw();
use NGCP::Panel::Utils::BillingMappings qw();
sub _item_rs {
my ($self, $c, $include_terminated,$now) = @_;
@ -23,12 +24,10 @@ sub _item_rs {
now => $now,
);
$item_rs = $item_rs->search({
'contact.reseller_id' => undef
},{
join => 'contact',
'+select' => 'billing_mappings.id',
'+as' => 'bmid',
});
'contact.reseller_id' => undef
},{
join => 'contact',
});
return $item_rs;
}
@ -41,10 +40,10 @@ sub get_form {
sub hal_from_contract {
my ($self, $c, $contract, $form, $now) = @_;
my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $contract, );
my $billing_profile_id = $billing_mapping->billing_profile_id;
my $future_billing_profiles = NGCP::Panel::Utils::Contract::resource_from_future_mappings($contract);
my $billing_profiles = NGCP::Panel::Utils::Contract::resource_from_mappings($contract);
my $future_billing_profiles = NGCP::Panel::Utils::BillingMappings::resource_from_future_mappings($contract);
my $billing_profiles = NGCP::Panel::Utils::BillingMappings::resource_from_mappings($contract);
#we leave this here to keep the former behaviour: contract balances are also created upon GET api/contracts/4711
NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
@ -111,15 +110,17 @@ sub contract_by_id {
sub update_contract {
my ($self, $c, $contract, $old_resource, $resource, $form, $now) = @_;
my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $contract, );
my $billing_profile = $billing_mapping->billing_profile;
$old_resource->{billing_mapping_id} = $billing_mapping->id;
my $old_package = $contract->profile_package;
$form //= $self->get_form($c);
# TODO: for some reason, formhandler lets missing contact_id slip thru
$resource->{contact_id} //= undef;
$resource->{type} //= $billing_mapping->product->class;
$resource->{type} //= $contract->product->class;
return unless $self->validate_form(
c => $c,
form => $form,
@ -132,7 +133,7 @@ sub update_contract {
my $mappings_to_create = [];
my $delete_mappings = 0;
my $set_package = ($resource->{billing_profile_definition} // 'id') eq 'package';
return unless NGCP::Panel::Utils::Contract::prepare_billing_mappings(
return unless NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings(
c => $c,
resource => $resource,
old_resource => $old_resource,
@ -165,7 +166,7 @@ sub update_contract {
try {
$contract->update($resource);
NGCP::Panel::Utils::Contract::remove_future_billing_mappings($contract,$now) if $delete_mappings;
NGCP::Panel::Utils::BillingMappings::remove_future_billing_mappings($contract,$now) if $delete_mappings;
foreach my $mapping (@$mappings_to_create) {
$contract->billing_mappings->create($mapping);
}
@ -183,7 +184,7 @@ sub update_contract {
profiles_added => ($set_package ? scalar @$mappings_to_create : 0),
);
$billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid'));
$billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $contract, );
$billing_profile = $billing_mapping->billing_profile;
if($old_resource->{status} ne $resource->{status}) {

@ -13,6 +13,7 @@ use HTTP::Status qw(:constants);
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::ProfilePackages qw();
use NGCP::Panel::Utils::BillingMappings qw();
use NGCP::Panel::Utils::Preferences;
use NGCP::Panel::Utils::Subscriber qw();
@ -23,7 +24,7 @@ sub _item_rs {
my $item_rs = NGCP::Panel::Utils::Contract::get_customer_rs(
c => $c,
include_terminated => 1,
now => $now,
#now => $now,
);
return $item_rs;
}
@ -41,10 +42,11 @@ sub hal_from_customer {
$is_adm = 1;
}
my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $customer, );
#my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
my $billing_profile_id = $billing_mapping->billing_profile->id;
my $future_billing_profiles = NGCP::Panel::Utils::Contract::resource_from_future_mappings($customer);
my $billing_profiles = NGCP::Panel::Utils::Contract::resource_from_mappings($customer);
my $future_billing_profiles = NGCP::Panel::Utils::BillingMappings::resource_from_future_mappings($customer);
my $billing_profiles = NGCP::Panel::Utils::BillingMappings::resource_from_mappings($customer);
NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c,
contract => $customer,
@ -130,17 +132,19 @@ sub update_customer {
return;
}
my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $customer, );
#my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
my $billing_profile = $billing_mapping->billing_profile;
my $old_package = $customer->profile_package;
$old_resource->{prepaid} = $billing_profile->prepaid;
$old_resource->{billing_mapping_id} = $billing_mapping->id;
$form //= $self->get_form($c);
# TODO: for some reason, formhandler lets missing contact_id slip thru
$resource->{contact_id} //= undef;
$resource->{type} //= $billing_mapping->product->class;
$resource->{type} //= $customer->product->class;
return unless $self->validate_form(
c => $c,
form => $form,
@ -154,7 +158,7 @@ sub update_customer {
my $mappings_to_create = [];
my $delete_mappings = 0;
my $set_package = ($resource->{billing_profile_definition} // 'id') eq 'package';
return unless NGCP::Panel::Utils::Contract::prepare_billing_mappings(
return unless NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings(
c => $c,
resource => $resource,
old_resource => $old_resource,
@ -211,7 +215,7 @@ sub update_customer {
try {
$customer->update($resource);
NGCP::Panel::Utils::Contract::remove_future_billing_mappings($customer,$now) if $delete_mappings;
NGCP::Panel::Utils::BillingMappings::remove_future_billing_mappings($customer,$now) if $delete_mappings;
foreach my $mapping (@$mappings_to_create) {
$customer->billing_mappings->create($mapping);
}
@ -229,7 +233,8 @@ sub update_customer {
profiles_added => ($set_package ? scalar @$mappings_to_create : 0),
);
$billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
$billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $customer, );
#$billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
$billing_profile = $billing_mapping->billing_profile;
if(($customer->external_id // '') ne $old_ext_id) {

@ -41,7 +41,7 @@ sub resource_from_item {
my %resource = %{ merge($bill_resource, $prov_resource) };
$resource{administrative} = delete $resource{admin};
unless($customer->get_column('product_class') eq 'pbxaccount') {
unless($customer->product->class eq 'pbxaccount') {
delete $resource{is_pbx_group};
delete $resource{is_pbx_pilot};
delete $resource{pbx_extension};
@ -70,7 +70,7 @@ sub resource_from_item {
run => 0,
);
if($customer->get_column('product_class') eq 'pbxaccount') {
if($customer->product->class eq 'pbxaccount') {
$resource{pbx_group_ids} = [];
foreach my $group($item->provisioning_voip_subscriber->voip_pbx_groups->search_rs(undef,{'order_by' => 'me.id'})->all) {
push @{ $resource{pbx_group_ids} }, int($group->group->voip_subscriber->id);
@ -261,10 +261,7 @@ sub get_customer {
'product.class' => 'sipaccount',
'product.class' => 'pbxaccount',
],
},{
'+select' => [ 'billing_mappings.id', 'product.class' ],
'+as' => [ 'bmid', 'product_class' ],
});
},undef);
if($c->user->roles eq "admin") {
} elsif($c->user->roles eq "reseller") {
$customer_rs = $customer_rs->search({
@ -280,11 +277,11 @@ sub get_customer {
}
sub get_billing_profile {
my ($self, $c, $customer) = @_;
my ($self, $c, $customer, $now) = @_;
my $mapping = $customer->billing_mappings->find($customer->get_column('bmid'));
if($mapping) {
return $mapping->billing_profile;
my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $customer, );
if($billing_mapping) {
return $billing_mapping->billing_profile;
} else {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'customer_id', doesn't have a valid billing mapping.");
return;
@ -397,7 +394,7 @@ sub prepare_resource {
}
my $pilot;
if($customer->get_column('product_class') eq 'pbxaccount') {
if($customer->product->class eq 'pbxaccount') {
$pilot = $customer->voip_subscribers->search({
'provisioning_voip_subscriber.is_pbx_pilot' => 1,
},{
@ -419,7 +416,7 @@ sub prepare_resource {
my $preferences = {};
my $admin = 0;
unless($customer->get_column('product_class') eq 'pbxaccount') {
unless($customer->product->class eq 'pbxaccount') {
for my $pref(qw/is_pbx_group pbx_extension pbx_hunt_policy pbx_hunt_timeout is_pbx_pilot/) {
delete $resource->{$pref};
}

@ -444,6 +444,8 @@ sub get_datatable_cols {
my ($c) = @_;
return (
{ name => "prepaid", "search" => 0, "title" => $c->loc("Prepaid"),
custom_renderer => 'function ( data, type, full ) { data.escapeHtml = false; return \'<input type="checkbox" disabled="disabled"\' + (full.prepaid == 1 ? \' checked="checked"\': \'\') + \'/>\'; }' },
{ name => "contract_cnt", "search" => 0, "title" => $c->loc("Used (contracts)"), },
{ name => "package_cnt", "search" => 0, "title" => $c->loc("Used (packages)"), },

@ -0,0 +1,542 @@
package NGCP::Panel::Utils::BillingMappings;
use strict;
use warnings;
use Sipwise::Base;
use NGCP::Panel::Utils::DateTime qw();
use DateTime::Format::Strptime qw();
sub get_actual_billing_mapping {
my %params = @_;
my ($c,$schema,$contract,$now) = @params{qw/c schema contract now/};
$schema //= $c->model('DB');
$now //= NGCP::Panel::Utils::DateTime::current_local;
my $contract_create = NGCP::Panel::Utils::DateTime::set_local_tz($contract->create_timestamp // $contract->modify_timestamp);
my $dtf = $schema->storage->datetime_parser;
$now = $contract_create if $now < $contract_create; #if there is no mapping starting with or before $now, it would returns the mapping with max(id):
my $bm_actual = $schema->resultset('billing_mappings_actual')->search({ contract_id => $contract->id },{bind => [ ( $dtf->format_datetime($now) ) x 2, ($contract->id) x 2 ],})->first;
if ($bm_actual) {
return $bm_actual->billing_mappings->first;
}
return undef;
}
sub get_actual_billing_mapping_stmt {
my %params = @_;
my ($c,$schema,$contract,$now,$projection,$contract_id_alias) = @params{qw/c schema contract now projection contract_id_alias/};
$schema //= $c->model('DB');
$now //= NGCP::Panel::Utils::DateTime::current_local;
my $dtf = $schema->storage->datetime_parser;
$projection //= 'actual_billing_mapping.id';
$contract_id_alias //= 'me.id';
return sprintf(<<EOS
select
%s
from
billing.billing_mappings actual_billing_mapping
join billing.billing_profiles billing_profile on actual_billing_mapping.billing_profile_id = billing_profile.id
left join billing.billing_networks billing_network on actual_billing_mapping.network_id = billing_network.id
join billing.products product on actual_billing_mapping.product_id = product.id
where
actual_billing_mapping.contract_id = %s
and (actual_billing_mapping.end_date >= %s or actual_billing_mapping.end_date is null)
and (actual_billing_mapping.start_date <= %s or actual_billing_mapping.start_date is null)
order by actual_billing_mapping.start_date desc, actual_billing_mapping.id desc limit 1
EOS
, $projection, $contract_id_alias, ( map { '"'.$_.'"'; } ( $dtf->format_datetime($now), $dtf->format_datetime($now) ) ));
}
sub prepare_billing_mappings {
my (%params) = @_;
my ($c,$resource,$old_resource,$mappings_to_create,$now,$delete_mappings,$err_code,$billing_profile_field,$billing_profiles_field,$profile_package_field,$billing_profile_definition_field) = @params{qw/c resource old_resource mappings_to_create now delete_mappings err_code billing_profile_field billing_profiles_field profile_package_field billing_profile_definition_field/};
my $schema = $c->model('DB');
if (!defined $err_code || ref $err_code ne 'CODE') {
$err_code = sub { return 0; };
}
my $profile_def_mode = $resource->{billing_profile_definition} // 'id';
$now //= NGCP::Panel::Utils::DateTime::current_local;
my $reseller_id = undef;
my $is_customer = 1;
if (defined $resource->{contact_id}) {
my $contact = $schema->resultset('contacts')->find($resource->{contact_id});
if ($contact) {
$reseller_id = $contact->reseller_id; #($contact->reseller_id // -1);
$is_customer = defined $reseller_id;
}
}
my $product_id = undef; #any subsequent create will fail without product_id
my $prepaid = undef;
my $billing_profile_id = undef;
if (defined $old_resource) {
# TODO: what about changed product, do we allow it?
my $billing_mapping;
if (exists $old_resource->{billing_mapping_id}) {
$billing_mapping = $schema->resultset('billing_mappings')->find($old_resource->{billing_mapping_id});
} elsif (exists $old_resource->{id}) {
$billing_mapping = get_actual_billing_mapping(schema => $schema,
contract => $schema->resultset('contracts')->find($old_resource->{id}),
now => $now);
#} else {
# return 0 unless &{$err_code}("No billing mapping or contract defined");
}
$product_id = $billing_mapping->contract->product->id;
$prepaid = $billing_mapping->billing_profile->prepaid;
$billing_profile_id = $billing_mapping->billing_profile->id;
} else {
if (exists $resource->{type} || exists $c->stash->{type}) {
my $productclass = (exists $c->stash->{type} ? $c->stash->{type} : $resource->{type});
my $product = $schema->resultset('products')->search_rs({ class => $productclass })->first;
if ($product) {
$product_id = $product->id;
}
} elsif (exists $resource->{product_id}) {
$product_id = $resource->{product_id};
}
}
if ('id' eq $profile_def_mode) {
my $delete = undef;
if (defined $old_resource) { #update
if (defined $resource->{billing_profile_id}) {
if ($billing_profile_id != $resource->{billing_profile_id}) {
#change profile:
$delete = 0; #1; #delete future mappings?
my $entities = {};
return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
resource => $resource,
profile_id_field => 'billing_profile_id',
field => $billing_profile_field,
);
my ($profile) = @$entities{qw/profile/};
push(@$mappings_to_create,{billing_profile_id => $profile->id,
network_id => undef,
product_id => $product_id,
start_date => $now,
end_date => undef,
});
} else {
#not changed, don't touch mappings
$delete = 0;
}
} else {
#undef profile is not allowed
$delete = 0;
my $entities = {};
return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
resource => $resource,
profile_id_field => 'billing_profile_id',
field => $billing_profile_field,
);
}
} else { #create
$delete = 1; #for the sake of completeness
my $entities = {};
return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
resource => $resource,
profile_id_field => 'billing_profile_id',
field => $billing_profile_field,
);
my ($profile) = @$entities{qw/profile/};
push(@$mappings_to_create,{billing_profile_id => $profile->id,
network_id => undef,
product_id => $product_id,
#we don't change the former behaviour in update situations:
start_date => undef,
end_date => undef,
});
}
if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') {
$$delete_mappings = $delete;
}
delete $resource->{profile_package_id};
} elsif ('profiles' eq $profile_def_mode) {
if (!defined $resource->{billing_profiles}) {
$resource->{billing_profiles} //= [];
}
if (ref $resource->{billing_profiles} ne "ARRAY") {
return 0 unless &{$err_code}("Invalid field 'billing_profiles'. Must be an array.",$billing_profiles_field);
}
my %interval_type_counts = ( open => 0, open_any_network => 0, 'open end' => 0, 'open start' => 0, 'start-end' => 0 );
my $dtf = $schema->storage->datetime_parser;
foreach my $mapping (@{$resource->{billing_profiles}}) {
if (ref $mapping ne "HASH") {
return 0 unless &{$err_code}("Invalid element in array 'billing_profiles'. Must be an object.",$billing_profiles_field);
}
my $entities = {};
return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
resource => $mapping,
field => $billing_profiles_field,
profile_id_field => 'profile_id',
network_id_field => 'network_id',
);
my ($profile,$network) = @$entities{qw/profile network/};
if (defined $prepaid) {
if ($profile->prepaid != $prepaid) {
return 0 unless &{$err_code}("Future switching between prepaid and post-paid billing profiles is not supported (" . $profile->name . ").",$billing_profiles_field);
}
} else {
$prepaid = $profile->prepaid;
}
# TODO: what about changed product, do we allow it?
#my $product_class = delete $mapping->{type};
#unless( (defined $product_class ) && ($product_class eq "sipaccount" || $product_class eq "pbxaccount") ) {
# return 0 unless &{$err_code}("Mandatory 'type' parameter is empty or invalid, must be 'sipaccount' or 'pbxaccount'.");
#}
#my $product = $schema->resultset('products')->search_res({ class => $product_class })->first;
#unless($product) {
# return 0 unless &{$err_code}("Invalid 'type'.");
#} else {
# # add product_id just for form check (not part of the actual contract item)
# # and remove it after the check
# $mapping->{product_id} = $product->id;
#}
my $start = (defined $mapping->{start} ? NGCP::Panel::Utils::DateTime::from_string($mapping->{start}) : undef);
my $stop = (defined $mapping->{stop} ? NGCP::Panel::Utils::DateTime::from_string($mapping->{stop}) : undef);
if (!defined $start && !defined $stop) { #open interval
$interval_type_counts{open} += 1;
$interval_type_counts{open_any_network} += 1 unless $network;
} elsif (defined $start && !defined $stop) { #open end interval
my $start_str = $dtf->format_datetime($start);
if ($start <= $now) {
return 0 unless &{$err_code}("'start' timestamp ($start_str) is not in future.",$billing_profiles_field);
}
#if (exists $start_dupes{$start_str}) {
# $start_dupes{$start_str} += 1;
# return 0 unless &{$err_code}("Identical 'start' timestamps ($start_str) not allowed.");
#} else {
# $start_dupes{$start_str} = 1;
#}
$interval_type_counts{'open end'} += 1;
} elsif (!defined $start && defined $stop) { #open start interval
my $stop_str = $dtf->format_datetime($stop);
return 0 unless &{$err_code}("Interval with 'stop' timestamp ($stop_str) but no 'start' timestamp specified.",$billing_profiles_field);
$interval_type_counts{'open start'} //= 0;
$interval_type_counts{'open start'} += 1;
} else { #start-end interval
my $start_str = $dtf->format_datetime($start);
if ($start <= $now) {
return 0 unless &{$err_code}("'start' timestamp ($start_str) is not in future.",$billing_profiles_field);
}
my $stop_str = $dtf->format_datetime($stop);
if ($start >= $stop) {
return 0 unless &{$err_code}("'start' timestamp ($start_str) has to be before 'stop' timestamp ($stop_str).",$billing_profiles_field);
}
#if (exists $start_dupes{$start_str}) {
# $start_dupes{$start_str} += 1;
# return 0 unless &{$err_code}("Identical 'start' timestamps ($start_str) not allowed.");
#} else {
# $start_dupes{$start_str} = 1;
#}
$interval_type_counts{'start-end'} += 1;
}
push(@$mappings_to_create,{
billing_profile_id => $profile->id,
network_id => ($is_customer && defined $network ? $network->id : undef),
product_id => $product_id,
start_date => $start,
end_date => $stop,
});
}
if (!defined $old_resource && $interval_type_counts{'open_any_network'} < 1) {
return 0 unless &{$err_code}("An initial interval without 'start' and 'stop' timestamps and no billing network is required.",$billing_profiles_field);
} elsif (defined $old_resource && $interval_type_counts{'open'} > 0) {
return 0 unless &{$err_code}("Adding intervals without 'start' and 'stop' timestamps is not allowed.",$billing_profiles_field);
}
if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') {
$$delete_mappings = 1; #always clear future mappings to place new ones
}
delete $resource->{profile_package_id};
} elsif ('package' eq $profile_def_mode) {
if (!$is_customer) {
return 0 unless &{$err_code}("Setting a profile package is supported for customer contracts only.",$billing_profile_definition_field);
}
my $delete = undef;
if (defined $old_resource) { #update
if (defined $old_resource->{profile_package_id} && !defined $resource->{profile_package_id}) {
#clear package: don't touch billing mappings (just clear profile package)
$delete = 0;
} elsif (!defined $old_resource->{profile_package_id} && defined $resource->{profile_package_id}) {
#set package: apply initial mappings
$delete = 0; #1; #delete future mappings?
my $entities = {};
return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
package_id => $resource->{profile_package_id},
field => $profile_package_field,
);
my ($package) = @$entities{qw/package/};
foreach my $mapping ($package->initial_profiles->all) {
push(@$mappings_to_create,{ #assume not terminated,
billing_profile_id => $mapping->profile_id,
network_id => ($is_customer ? $mapping->network_id : undef),
product_id => $product_id,
start_date => $now,
end_date => undef,
});
}
} elsif (defined $old_resource->{profile_package_id} && defined $resource->{profile_package_id}) {
if ($old_resource->{profile_package_id} != $resource->{profile_package_id}) {
#change package: apply initial mappings
$delete = 0; #1; #delete future mappings?
my $entities = {};
return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
package_id => $resource->{profile_package_id},
field => $profile_package_field,
);
my ($package) = @$entities{qw/package/};
foreach my $mapping ($package->initial_profiles->all) {
push(@$mappings_to_create,{ #assume not terminated,
billing_profile_id => $mapping->profile_id,
network_id => ($is_customer ? $mapping->network_id : undef),
product_id => $product_id,
start_date => $now,
end_date => undef,
});
}
} else {
#package unchanged: don't touch billing mappings
$delete = 0;
}
} else {
#package unchanged (null): don't touch billing mappings
$delete = 0;
}
} else { #create
$delete = 1; #for the sake of completeness
my $entities = {};
return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
package_id => $resource->{profile_package_id},
field => $profile_package_field,
);
my ($package) = @$entities{qw/package/};
foreach my $mapping ($package->initial_profiles->all) {
push(@$mappings_to_create,{ #assume not terminated,
billing_profile_id => $mapping->profile_id,
network_id => ($is_customer ? $mapping->network_id : undef),
product_id => $product_id,
start_date => undef, #$now,
end_date => undef,
});
}
}
if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') {
$$delete_mappings = $delete;
}
} else {
return 0 unless &{$err_code}("Invalid 'billing_profile_definition'.",$billing_profile_definition_field);
}
delete $resource->{billing_profile_id};
delete $resource->{billing_profiles};
delete $resource->{billing_profile_definition};
return 1;
}
sub _check_profile_network {
my (%params) = @_;
my ($c,$res,$profile_id_field,$network_id_field,$field,$reseller_id,$err_code,$entities) = @params{qw/c resource profile_id_field network_id_field field reseller_id err_code entities/};
my $schema = $c->model('DB');
if (!defined $err_code || ref $err_code ne 'CODE') {
$err_code = sub { return 0; };
}
unless(defined $res->{$profile_id_field}) {
return 0 unless &{$err_code}("Invalid '$profile_id_field', not defined.",$field);
}
my $profile = $schema->resultset('billing_profiles')->find($res->{$profile_id_field});
unless($profile) {
return 0 unless &{$err_code}("Invalid '$profile_id_field' ($res->{$profile_id_field}).",$field);
}
if ($profile->status eq 'terminated') {
return 0 unless &{$err_code}("Invalid '$profile_id_field' ($res->{$profile_id_field}), already terminated.",$field);
}
if (defined $reseller_id && defined $profile->reseller_id && $reseller_id != $profile->reseller_id) { #($profile->reseller_id // -1)) {
return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the billing profile (" . $profile->name . ").",$field);
}
my $network;
if (defined $network_id_field && defined $res->{$network_id_field}) {
$network = $schema->resultset('billing_networks')->find($res->{$network_id_field});
unless($network) {
return 0 unless &{$err_code}("Invalid '$network_id_field' ($res->{$network_id_field}).",$field);
}
if (defined $reseller_id && defined $network->reseller_id && $reseller_id != $network->reseller_id) { #($network->reseller_id // -1)) {
return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the billing network (" . $network->name . ").",$field);
}
}
if (defined $entities and ref $entities eq 'HASH') {
$entities->{profile} = $profile;
$entities->{network} = $network;
}
return 1;
}
sub _check_profile_package {
my (%params) = @_;
my ($c,$res,$package_id,$reseller_id,$field,$err_code,$entities) = @params{qw/c resource package_id reseller_id field err_code entities/};
my $schema = $c->model('DB');
if (!defined $err_code || ref $err_code ne 'CODE') {
$err_code = sub { return 0; };
}
unless(defined $package_id) {
return 0 unless &{$err_code}("Invalid 'profile_package_id', not defined.",$field);
}
my $package = $schema->resultset('profile_packages')->find($package_id);
unless($package) {
return 0 unless &{$err_code}("Invalid 'profile_package_id'.",$field);
}
if (defined $reseller_id && defined $package->reseller_id && $reseller_id != $package->reseller_id) {
return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the profile package (" . $package->name . ").",$field);
}
if (defined $entities and ref $entities eq 'HASH') {
$entities->{package} = $package;
}
return 1;
}
sub resource_from_future_mappings {
my ($contract) = @_;
return resource_from_mappings($contract,1);
}
sub resource_from_mappings {
my ($contract,$future_only) = @_;
my $is_customer = (defined $contract->contact->reseller_id ? 1 : 0);
my @mappings_resource = ();
my $datetime_fmt = DateTime::Format::Strptime->new(
pattern => '%F %T',
); #validate_forms uses RFC3339 otherwise, which contains the tz offset part
foreach my $mapping (billing_mappings_ordered($future_only ? future_billing_mappings($contract->billing_mappings) : $contract->billing_mappings)->all) {
my %m = $mapping->get_inflated_columns;
delete $m{id};
$m{start} = delete $m{start_date};
$m{stop} = delete $m{end_date};
$m{start} = $datetime_fmt->format_datetime($m{start}) if defined $m{start};
$m{stop} = $datetime_fmt->format_datetime($m{stop}) if defined $m{stop};
$m{profile_id} = delete $m{billing_profile_id};
delete $m{contract_id};
delete $m{product_id};
delete $m{network_id} unless $is_customer;
push(@mappings_resource,\%m);
}
return \@mappings_resource;
}
sub billing_mappings_ordered {
my ($rs,$now,$actual_bmid) = @_;
my $dtf;
$dtf = $rs->result_source->schema->storage->datetime_parser if defined $now;
my @select = ();
if ($now) {
push(@select,{ '' => \[ 'if(`me`.`start_date` is null,0,`me`.`start_date` > ?)', $dtf->format_datetime($now) ], -as => 'is_future' });
}
if ($actual_bmid) {
push(@select,{ '' => \[ '`me`.`id` = ?', $actual_bmid ], -as => 'is_actual' });
}
return $rs->search_rs(
{},
{ order_by => { '-asc' => ['start_date', 'id']},
(scalar @select == 1 ? ('+select' => $select[0]) : ()),
(scalar @select > 1 ? ('+select' => \@select) : ()),
});
}
sub remove_future_billing_mappings {
my ($contract,$now) = @_;
$now //= NGCP::Panel::Utils::DateTime::current_local;
future_billing_mappings($contract->billing_mappings,$now)->delete;
}
sub future_billing_mappings {
my ($rs,$now) = @_;
$now //= NGCP::Panel::Utils::DateTime::current_local;
return $rs->search_rs({start_date => { '>' => $now },});
}
sub get_billingmappings_timeline_data {
my ($c,$contract,$range) = @_;
unless ($range) {
$range = eval { $c->req->body_data; };
if ($@) {
$c->log->error('error decoding timeline json request: ' . $@);
}
}
my $start;
$start = NGCP::Panel::Utils::DateTime::from_string($range->{start}) if $range->{start};
my $end;
$end = NGCP::Panel::Utils::DateTime::from_string($range->{end}) if $range->{end};
$c->log->debug("timeline range $start - $end");
#the max start date (of mappings with NULL end date) less than
#the visible range end will become the range start:
my $max_start_date = $contract->billing_mappings->search({
($end ? (start_date => [ -or =>
{ '<=' => $end },
{ '=' => undef },
]) : ()),
end_date => { '=' => undef },
},{
order_by => { '-desc' => ['start_date', 'me.id']}, #NULL start dates at last
})->first;
#lower the range start, if required:
if ($max_start_date) {
if ($max_start_date->start_date) {
$start = $max_start_date->start_date if (not $start or $max_start_date->start_date < $start);
} else {
$start = $max_start_date->start_date;
}
}
my $res = $contract->billing_mappings->search({
($end ? (start_date => ($start ? [ -and => {
'<=' => $end },{ #hide mappings beginning after range end
'>=' => $start #and beginning before range start (max_start_date).
},] : [ -or => { #if there is a mapping with NULL start only,
'<=' => $end },{ #include all mapping beginning before range end.
'=' => undef
},])) : ()),
},{
order_by => { '-asc' => ['start_date', 'me.id']},
prefetch => [ 'billing_profile' , 'network' ]
});
my @timeline_events = map {
{ $_->get_columns,
billing_profile => { ($_->billing_profile ? ( name => $_->billing_profile->name, ) : ()) },
network => { ($_->network ? ( name => $_->network->name, ) : ()) },
};
} $res->all;
return \@timeline_events;
}
1;

@ -5,7 +5,7 @@ use warnings;
use Sipwise::Base;
use DBIx::Class::Exception;
use NGCP::Panel::Utils::DateTime;
use DateTime::Format::Strptime qw();
#use DateTime::Format::Strptime qw();
use NGCP::Panel::Utils::CallList qw();
sub recursively_lock_contract {
@ -108,71 +108,48 @@ sub recursively_lock_contract {
}
sub get_contract_rs {
my %params = @_;
my ($schema,$now,$contract_id) = @params{qw/schema now contract_id/};
$now //= NGCP::Panel::Utils::DateTime::current_local;
my $dtf = $schema->storage->datetime_parser;
my $rs = $schema->resultset('contracts')
->search({
$params{include_terminated} ? () : ('me.status' => { '!=' => 'terminated' }),
},{
bind => [ ( $dtf->format_datetime($now) ) x 2, ( $contract_id ) x 2 ],
'join' => { 'billing_mappings_actual' => { 'billing_mappings' => 'product'}},
'+select' => [
'billing_mappings.id',
'billing_mappings.start_date',
'billing_mappings.product_id',
],
'+as' => [
'billing_mapping_id',
'billing_mapping_start_date',
'product_id',
],
alias => 'me',
});
my %params = @_;
my ($c,$schema,$include_terminated) = @params{qw/c schema include_terminated/};
$schema //= $c->model('DB');
my $rs = $schema->resultset('contracts')->search({
$include_terminated ? () : ('me.status' => { '!=' => 'terminated' }),
},{
join => 'product',
});
return $rs;
}
sub get_customer_rs {
my %params = @_;
my ($c,$now,$contract_id) = @params{qw/c now contract_id/};
my $customers = get_contract_rs(
schema => $c->model('DB'),
include_terminated => $params{include_terminated},
now => $now,
contract_id => $contract_id,
);
$customers = $customers->search({
'contact.reseller_id' => { '-not' => undef },
my ($c,$schema,$include_terminated) = @params{qw/c schema include_terminated/};
my $rs = get_contract_rs(
c => $c,
schema => $schema,
include_terminated => $include_terminated,
)->search_rs({
'product.class' => { -in => [ 'sipaccount', 'pbxaccount' ] },
},{
join => 'contact',
});
if($c->user->roles eq "admin") {
$rs = $rs->search_rs({
'contact.reseller_id' => { '-not' => undef },
},undef);
} elsif($c->user->roles eq "reseller") {
$customers = $customers->search({
'contact.reseller_id' => $c->user->reseller_id,
});
} elsif($c->user->roles eq "subscriberadmin") {
$customers = $customers->search({
'contact.reseller_id' => $c->user->contract->contact->reseller_id,
});
$rs = $rs->search({
'contact.reseller_id' => $c->user->reseller_id,
},undef);
} elsif($c->user->roles eq "subscriberadmin" or $c->user->roles eq "subscriber") {
$rs = $rs->search({
'contact.reseller_id' => $c->user->contract->contact->reseller_id,
},undef);
}
$customers = $customers->search({
'-or' => [
'product.class' => 'sipaccount',
'product.class' => 'pbxaccount',
],
},{
'+select' => 'billing_mappings.id',
'+as' => 'bmid',
});
return $customers;
return $rs;
}
sub get_contract_zonesfees_rs {
@ -327,488 +304,6 @@ sub get_contract_calls_rs{
}
sub prepare_billing_mappings {
my (%params) = @_;
my ($c,$resource,$old_resource,$mappings_to_create,$now,$delete_mappings,$err_code,$billing_profile_field,$billing_profiles_field,$profile_package_field,$billing_profile_definition_field) = @params{qw/c resource old_resource mappings_to_create now delete_mappings err_code billing_profile_field billing_profiles_field profile_package_field billing_profile_definition_field/};
my $schema = $c->model('DB');
if (!defined $err_code || ref $err_code ne 'CODE') {
$err_code = sub { return 0; };
}
my $profile_def_mode = $resource->{billing_profile_definition} // 'id';
$now //= NGCP::Panel::Utils::DateTime::current_local;
my $reseller_id = undef;
my $is_customer = 1;
if (defined $resource->{contact_id}) {
my $contact = $schema->resultset('contacts')->find($resource->{contact_id});
if ($contact) {
$reseller_id = $contact->reseller_id; #($contact->reseller_id // -1);
$is_customer = defined $reseller_id;
}
}
my $product_id = undef; #any subsequent create will fail without product_id
my $prepaid = undef;
my $billing_profile_id = undef;
if (defined $old_resource) {
# TODO: what about changed product, do we allow it?
my $billing_mapping = $schema->resultset('billing_mappings')->find($old_resource->{billing_mapping_id});
$product_id = $billing_mapping->product->id;
$prepaid = $billing_mapping->billing_profile->prepaid;
$billing_profile_id = $billing_mapping->billing_profile->id;
} else {
if (exists $resource->{type} || exists $c->stash->{type}) {
my $productclass = (exists $c->stash->{type} ? $c->stash->{type} : $resource->{type});
my $product = $schema->resultset('products')->find({ class => $productclass });
if ($product) {
$product_id = $product->id;
}
} elsif (exists $resource->{product_id}) {
$product_id = $resource->{product_id};
}
}
if ('id' eq $profile_def_mode) {
my $delete = undef;
if (defined $old_resource) { #update
if (defined $resource->{billing_profile_id}) {
if ($billing_profile_id != $resource->{billing_profile_id}) {
#change profile:
$delete = 0; #1; #delete future mappings?
my $entities = {};
return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
resource => $resource,
profile_id_field => 'billing_profile_id',
field => $billing_profile_field,
);
my ($profile) = @$entities{qw/profile/};
push(@$mappings_to_create,{billing_profile_id => $profile->id,
network_id => undef,
product_id => $product_id,
start_date => $now,
end_date => undef,
});
} else {
#not changed, don't touch mappings
$delete = 0;
}
} else {
#undef profile is not allowed
$delete = 0;
my $entities = {};
return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
resource => $resource,
profile_id_field => 'billing_profile_id',
field => $billing_profile_field,
);
}
} else { #create
$delete = 1; #for the sake of completeness
my $entities = {};
return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
resource => $resource,
profile_id_field => 'billing_profile_id',
field => $billing_profile_field,
);
my ($profile) = @$entities{qw/profile/};
push(@$mappings_to_create,{billing_profile_id => $profile->id,
network_id => undef,
product_id => $product_id,
#we don't change the former behaviour in update situations:
start_date => undef,
end_date => undef,
});
}
if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') {
$$delete_mappings = $delete;
}
delete $resource->{profile_package_id};
} elsif ('profiles' eq $profile_def_mode) {
if (!defined $resource->{billing_profiles}) {
$resource->{billing_profiles} //= [];
}
if (ref $resource->{billing_profiles} ne "ARRAY") {
return 0 unless &{$err_code}("Invalid field 'billing_profiles'. Must be an array.",$billing_profiles_field);
}
my %interval_type_counts = ( open => 0, open_any_network => 0, 'open end' => 0, 'open start' => 0, 'start-end' => 0 );
my $dtf = $schema->storage->datetime_parser;
foreach my $mapping (@{$resource->{billing_profiles}}) {
if (ref $mapping ne "HASH") {
return 0 unless &{$err_code}("Invalid element in array 'billing_profiles'. Must be an object.",$billing_profiles_field);
}
my $entities = {};
return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
resource => $mapping,
field => $billing_profiles_field,
profile_id_field => 'profile_id',
network_id_field => 'network_id',
);
my ($profile,$network) = @$entities{qw/profile network/};
if (defined $prepaid) {
if ($profile->prepaid != $prepaid) {
return 0 unless &{$err_code}("Future switching between prepaid and post-paid billing profiles is not supported (" . $profile->name . ").",$billing_profiles_field);
}
} else {
$prepaid = $profile->prepaid;
}
# TODO: what about changed product, do we allow it?
#my $product_class = delete $mapping->{type};
#unless( (defined $product_class ) && ($product_class eq "sipaccount" || $product_class eq "pbxaccount") ) {
# return 0 unless &{$err_code}("Mandatory 'type' parameter is empty or invalid, must be 'sipaccount' or 'pbxaccount'.");
#}
#my $product = $schema->resultset('products')->find({ class => $product_class });
#unless($product) {
# return 0 unless &{$err_code}("Invalid 'type'.");
#} else {
# # add product_id just for form check (not part of the actual contract item)
# # and remove it after the check
# $mapping->{product_id} = $product->id;
#}
my $start = (defined $mapping->{start} ? NGCP::Panel::Utils::DateTime::from_string($mapping->{start}) : undef);
my $stop = (defined $mapping->{stop} ? NGCP::Panel::Utils::DateTime::from_string($mapping->{stop}) : undef);
if (!defined $start && !defined $stop) { #open interval
$interval_type_counts{open} += 1;
$interval_type_counts{open_any_network} += 1 unless $network;
} elsif (defined $start && !defined $stop) { #open end interval
my $start_str = $dtf->format_datetime($start);
if ($start <= $now) {
return 0 unless &{$err_code}("'start' timestamp ($start_str) is not in future.",$billing_profiles_field);
}
#if (exists $start_dupes{$start_str}) {
# $start_dupes{$start_str} += 1;
# return 0 unless &{$err_code}("Identical 'start' timestamps ($start_str) not allowed.");
#} else {
# $start_dupes{$start_str} = 1;
#}
$interval_type_counts{'open end'} += 1;
} elsif (!defined $start && defined $stop) { #open start interval
my $stop_str = $dtf->format_datetime($stop);
return 0 unless &{$err_code}("Interval with 'stop' timestamp ($stop_str) but no 'start' timestamp specified.",$billing_profiles_field);
$interval_type_counts{'open start'} //= 0;
$interval_type_counts{'open start'} += 1;
} else { #start-end interval
my $start_str = $dtf->format_datetime($start);
if ($start <= $now) {
return 0 unless &{$err_code}("'start' timestamp ($start_str) is not in future.",$billing_profiles_field);
}
my $stop_str = $dtf->format_datetime($stop);
if ($start >= $stop) {
return 0 unless &{$err_code}("'start' timestamp ($start_str) has to be before 'stop' timestamp ($stop_str).",$billing_profiles_field);
}
#if (exists $start_dupes{$start_str}) {
# $start_dupes{$start_str} += 1;
# return 0 unless &{$err_code}("Identical 'start' timestamps ($start_str) not allowed.");
#} else {
# $start_dupes{$start_str} = 1;
#}
$interval_type_counts{'start-end'} += 1;
}
push(@$mappings_to_create,{
billing_profile_id => $profile->id,
network_id => ($is_customer && defined $network ? $network->id : undef),
product_id => $product_id,
start_date => $start,
end_date => $stop,
});
}
if (!defined $old_resource && $interval_type_counts{'open_any_network'} < 1) {
return 0 unless &{$err_code}("An initial interval without 'start' and 'stop' timestamps and no billing network is required.",$billing_profiles_field);
} elsif (defined $old_resource && $interval_type_counts{'open'} > 0) {
return 0 unless &{$err_code}("Adding intervals without 'start' and 'stop' timestamps is not allowed.",$billing_profiles_field);
}
if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') {
$$delete_mappings = 1; #always clear future mappings to place new ones
}
delete $resource->{profile_package_id};
} elsif ('package' eq $profile_def_mode) {
if (!$is_customer) {
return 0 unless &{$err_code}("Setting a profile package is supported for customer contracts only.",$billing_profile_definition_field);
}
my $delete = undef;
if (defined $old_resource) { #update
if (defined $old_resource->{profile_package_id} && !defined $resource->{profile_package_id}) {
#clear package: don't touch billing mappings (just clear profile package)
$delete = 0;
} elsif (!defined $old_resource->{profile_package_id} && defined $resource->{profile_package_id}) {
#set package: apply initial mappings
$delete = 0; #1; #delete future mappings?
my $entities = {};
return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
package_id => $resource->{profile_package_id},
field => $profile_package_field,
);
my ($package) = @$entities{qw/package/};
foreach my $mapping ($package->initial_profiles->all) {
push(@$mappings_to_create,{ #assume not terminated,
billing_profile_id => $mapping->profile_id,
network_id => ($is_customer ? $mapping->network_id : undef),
product_id => $product_id,
start_date => $now,
end_date => undef,
});
}
} elsif (defined $old_resource->{profile_package_id} && defined $resource->{profile_package_id}) {
if ($old_resource->{profile_package_id} != $resource->{profile_package_id}) {
#change package: apply initial mappings
$delete = 0; #1; #delete future mappings?
my $entities = {};
return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
package_id => $resource->{profile_package_id},
field => $profile_package_field,
);
my ($package) = @$entities{qw/package/};
foreach my $mapping ($package->initial_profiles->all) {
push(@$mappings_to_create,{ #assume not terminated,
billing_profile_id => $mapping->profile_id,
network_id => ($is_customer ? $mapping->network_id : undef),
product_id => $product_id,
start_date => $now,
end_date => undef,
});
}
} else {
#package unchanged: don't touch billing mappings
$delete = 0;
}
} else {
#package unchanged (null): don't touch billing mappings
$delete = 0;
}
} else { #create
$delete = 1; #for the sake of completeness
my $entities = {};
return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities,
package_id => $resource->{profile_package_id},
field => $profile_package_field,
);
my ($package) = @$entities{qw/package/};
foreach my $mapping ($package->initial_profiles->all) {
push(@$mappings_to_create,{ #assume not terminated,
billing_profile_id => $mapping->profile_id,
network_id => ($is_customer ? $mapping->network_id : undef),
product_id => $product_id,
start_date => undef, #$now,
end_date => undef,
});
}
}
if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') {
$$delete_mappings = $delete;
}
} else {
return 0 unless &{$err_code}("Invalid 'billing_profile_definition'.",$billing_profile_definition_field);
}
delete $resource->{billing_profile_id};
delete $resource->{billing_profiles};
delete $resource->{billing_profile_definition};
return 1;
}
sub _check_profile_network {
my (%params) = @_;
my ($c,$res,$profile_id_field,$network_id_field,$field,$reseller_id,$err_code,$entities) = @params{qw/c resource profile_id_field network_id_field field reseller_id err_code entities/};
my $schema = $c->model('DB');
if (!defined $err_code || ref $err_code ne 'CODE') {
$err_code = sub { return 0; };
}
unless(defined $res->{$profile_id_field}) {
return 0 unless &{$err_code}("Invalid '$profile_id_field', not defined.",$field);
}
my $profile = $schema->resultset('billing_profiles')->find($res->{$profile_id_field});
unless($profile) {
return 0 unless &{$err_code}("Invalid '$profile_id_field' ($res->{$profile_id_field}).",$field);
}
if ($profile->status eq 'terminated') {
return 0 unless &{$err_code}("Invalid '$profile_id_field' ($res->{$profile_id_field}), already terminated.",$field);
}
if (defined $reseller_id && defined $profile->reseller_id && $reseller_id != $profile->reseller_id) { #($profile->reseller_id // -1)) {
return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the billing profile (" . $profile->name . ").",$field);
}
my $network;
if (defined $network_id_field && defined $res->{$network_id_field}) {
$network = $schema->resultset('billing_networks')->find($res->{$network_id_field});
unless($network) {
return 0 unless &{$err_code}("Invalid '$network_id_field' ($res->{$network_id_field}).",$field);
}
if (defined $reseller_id && defined $network->reseller_id && $reseller_id != $network->reseller_id) { #($network->reseller_id // -1)) {
return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the billing network (" . $network->name . ").",$field);
}
}
if (defined $entities and ref $entities eq 'HASH') {
$entities->{profile} = $profile;
$entities->{network} = $network;
}
return 1;
}
sub _check_profile_package {
my (%params) = @_;
my ($c,$res,$package_id,$reseller_id,$field,$err_code,$entities) = @params{qw/c resource package_id reseller_id field err_code entities/};
my $schema = $c->model('DB');
if (!defined $err_code || ref $err_code ne 'CODE') {
$err_code = sub { return 0; };
}
unless(defined $package_id) {
return 0 unless &{$err_code}("Invalid 'profile_package_id', not defined.",$field);
}
my $package = $schema->resultset('profile_packages')->find($package_id);
unless($package) {
return 0 unless &{$err_code}("Invalid 'profile_package_id'.",$field);
}
if (defined $reseller_id && defined $package->reseller_id && $reseller_id != $package->reseller_id) {
return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the profile package (" . $package->name . ").",$field);
}
if (defined $entities and ref $entities eq 'HASH') {
$entities->{package} = $package;
}
return 1;
}
sub resource_from_future_mappings {
my ($contract) = @_;
return resource_from_mappings($contract,1);
}
sub resource_from_mappings {
my ($contract,$future_only) = @_;
my $is_customer = (defined $contract->contact->reseller_id ? 1 : 0);
my @mappings_resource = ();
my $datetime_fmt = DateTime::Format::Strptime->new(
pattern => '%F %T',
); #validate_forms uses RFC3339 otherwise, which contains the tz offset part
foreach my $mapping (billing_mappings_ordered($future_only ? future_billing_mappings($contract->billing_mappings) : $contract->billing_mappings)->all) {
my %m = $mapping->get_inflated_columns;
delete $m{id};
$m{start} = delete $m{start_date};
$m{stop} = delete $m{end_date};
$m{start} = $datetime_fmt->format_datetime($m{start}) if defined $m{start};
$m{stop} = $datetime_fmt->format_datetime($m{stop}) if defined $m{stop};
$m{profile_id} = delete $m{billing_profile_id};
delete $m{contract_id};
delete $m{product_id};
delete $m{network_id} unless $is_customer;
push(@mappings_resource,\%m);
}
return \@mappings_resource;
}
sub billing_mappings_ordered {
my ($rs,$now,$actual_bmid) = @_;
my $dtf;
$dtf = $rs->result_source->schema->storage->datetime_parser if defined $now;
my @select = ();
if ($now) {
push(@select,{ '' => \[ 'if(`me`.`start_date` is null,0,`me`.`start_date` > ?)', $dtf->format_datetime($now) ], -as => 'is_future' });
}
if ($actual_bmid) {
push(@select,{ '' => \[ '`me`.`id` = ?', $actual_bmid ], -as => 'is_actual' });
}
return $rs->search_rs(
{},
{ order_by => { '-asc' => ['start_date', 'id']},
(scalar @select == 1 ? ('+select' => $select[0]) : ()),
(scalar @select > 1 ? ('+select' => \@select) : ()),
});
}
sub remove_future_billing_mappings {
my ($contract,$now) = @_;
$now //= NGCP::Panel::Utils::DateTime::current_local;
future_billing_mappings($contract->billing_mappings,$now)->delete;
}
sub future_billing_mappings {
my ($rs,$now) = @_;
$now //= NGCP::Panel::Utils::DateTime::current_local;
return $rs->search_rs({start_date => { '>' => $now },});
}
sub get_billingmappings_timeline_data {
my ($c,$contract,$range) = @_;
unless ($range) {
$range = eval { $c->req->body_data; };
if ($@) {
$c->log->error('error decoding timeline json request: ' . $@);
}
}
my $start;
$start = NGCP::Panel::Utils::DateTime::from_string($range->{start}) if $range->{start};
my $end;
$end = NGCP::Panel::Utils::DateTime::from_string($range->{end}) if $range->{end};
$c->log->debug("timeline range $start - $end");
#the max start date (of mappings with NULL end date) less than
#the visible range end will become the range start:
my $max_start_date = $contract->billing_mappings->search({
($end ? (start_date => [ -or =>
{ '<=' => $end },
{ '=' => undef },
]) : ()),
end_date => { '=' => undef },
},{
order_by => { '-desc' => ['start_date', 'me.id']}, #NULL start dates at last
})->first;
#lower the range start, if required:
if ($max_start_date) {
if ($max_start_date->start_date) {
$start = $max_start_date->start_date if (not $start or $max_start_date->start_date < $start);
} else {
$start = $max_start_date->start_date;
}
}
my $res = $contract->billing_mappings->search({
($end ? (start_date => ($start ? [ -and => {
'<=' => $end },{ #hide mappings beginning after range end
'>=' => $start #and beginning before range start (max_start_date).
},] : [ -or => { #if there is a mapping with NULL start only,
'<=' => $end },{ #include all mapping beginning before range end.
'=' => undef
},])) : ()),
},{
order_by => { '-asc' => ['start_date', 'me.id']},
prefetch => [ 'billing_profile' , 'network' ]
});
my @timeline_events = map {
{ $_->get_columns,
billing_profile => { ($_->billing_profile ? ( name => $_->billing_profile->name, ) : ()) },
network => { ($_->network ? ( name => $_->network->name, ) : ()) },
};
} $res->all;
return \@timeline_events;
}
1;
__END__

@ -1,7 +1,8 @@
package NGCP::Panel::Utils::Invoice;
use Sipwise::Base;
use NGCP::Panel::Utils::ProfilePackages;
use NGCP::Panel::Utils::ProfilePackages qw();
use NGCP::Panel::Utils::BillingMappings qw();
use NGCP::Panel::Utils::InvoiceTemplate;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Utils::Message;
@ -18,12 +19,12 @@ sub get_invoice_amounts{
$billing_profile->{interval_charge} //= 0;
$customer_contract->{vat_rate} //= 0;
#use Data::Dumper;
#print Dumper [$contract_balance,$billing_profile];
#print Dumper [$contract_balance,$billing_profile];
$invoice->{amount_net} = $contract_balance->{cash_balance_interval} / 100 + $billing_profile->{interval_charge};
$invoice->{amount_vat} =
$customer_contract->{add_vat}
$invoice->{amount_vat} =
$customer_contract->{add_vat}
?
$invoice->{amount_net} * ($customer_contract->{vat_rate}/100)
$invoice->{amount_net} * ($customer_contract->{vat_rate}/100)
: 0,
$invoice->{amount_total} = $invoice->{amount_net} + $invoice->{amount_vat};
return $invoice;
@ -60,11 +61,11 @@ sub create_invoice{
etime => $etime,);
$stime = $balance->start;
$etime = $balance->end;
my $bm_actual = NGCP::Panel::Utils::ProfilePackages::get_actual_billing_mapping(
c => $c,
contract => $customer,
my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(
c => $c,
contract => $customer,
now => $balance->start);
my $billing_profile = $bm_actual->billing_mappings->first->billing_profile;
my $billing_profile = $bm_actual->billing_profile;
my $zonecalls = NGCP::Panel::Utils::Contract::get_contract_zonesfees(
c => $c,
contract_id => $contract_id,
@ -124,9 +125,9 @@ sub create_invoice{
try {
$invoice = $schema->resultset('invoices')->create($invoice_data);
} catch($e) {
die {
showdetails => $c->loc('Failed to save invoice meta data.'),
error => $e,
die {
showdetails => $c->loc('Failed to save invoice meta data.'),
error => $e,
httpcode => HTTP_UNPROCESSABLE_ENTITY,
};
}
@ -174,9 +175,9 @@ sub create_invoice{
my $error = $t->error();
my $error_msg = "error processing template, type=".$error->type.", info='".$error->info."'";
my $msg =$c->loc('Failed to render template. Type is [_1], info is [_2].', $error->type, $error->info);
die {
showdetails => $msg,
error => $error_msg,
die {
showdetails => $msg,
error => $error_msg,
httpcode => HTTP_UNPROCESSABLE_ENTITY,
};
};
@ -205,12 +206,11 @@ sub check_invoice_data{
my $invoice_data = {};
my $schema = $c->model('DB');
my $customer_rs = NGCP::Panel::Utils::Contract::get_customer_rs(c => $c, contract_id => $contract_id);
my $customer = $customer_rs->find({ 'me.id' => $contract_id });
my $customer = NGCP::Panel::Utils::Contract::get_customer_rs(c => $c)->find({ 'me.id' => $contract_id });
unless($customer) {
die {
showdetails => $c->loc('Customer not found'),
error => "invalid contract_id $contract_id",
die {
showdetails => $c->loc('Customer not found'),
error => "invalid contract_id $contract_id",
httpcode => HTTP_UNPROCESSABLE_ENTITY,
};
}
@ -229,24 +229,24 @@ sub check_invoice_data{
$tmpl = $tmpl->first;
unless($tmpl) {
die {
showdetails => $c->loc('Invoice template not found'),
error => "invalid template id $tmpl_id",
die {
showdetails => $c->loc('Invoice template not found'),
error => "invalid template id $tmpl_id",
httpcode => HTTP_UNPROCESSABLE_ENTITY,
};
}
unless($tmpl->data) {
die {
showdetails => $c->loc('Invoice template does not have an SVG stored yet'),
error => "invalid template id $tmpl_id, data is empty",
die {
showdetails => $c->loc('Invoice template does not have an SVG stored yet'),
error => "invalid template id $tmpl_id, data is empty",
httpcode => HTTP_UNPROCESSABLE_ENTITY,
};
}
unless($customer->contact->reseller_id == $tmpl->reseller_id) {
die {
showdetails => $c->loc('Template and customer must belong to same reseller'),
error => "template id ".$tmpl->id." has different reseller than contract id $contract_id",
die {
showdetails => $c->loc('Template and customer must belong to same reseller'),
error => "template id ".$tmpl->id." has different reseller than contract id $contract_id",
httpcode => HTTP_UNPROCESSABLE_ENTITY,
};
}

@ -6,6 +6,7 @@ use Scalar::Util qw(looks_like_number);
#use TryCatch;
use NGCP::Panel::Utils::DateTime qw();
use NGCP::Panel::Utils::Subscriber qw();
use NGCP::Panel::Utils::BillingMappings qw();
use Data::Dumper;
use constant INITIAL_PROFILE_DISCRIMINATOR => 'initial';
@ -261,13 +262,13 @@ sub catchup_contract_balances {
my $bm_actual;
unless ($last_profile) {
$bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => NGCP::Panel::Utils::DateTime::set_local_tz($last_balance->start));
$last_profile = $bm_actual->billing_mappings->first->billing_profile;
$bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => NGCP::Panel::Utils::DateTime::set_local_tz($last_balance->start));
$last_profile = $bm_actual->billing_profile;
}
my ($underrun_profiles_ts,$underrun_lock_ts) = (undef,undef);
PREPARE_BALANCE_CATCHUP:
$bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $start_of_next_interval);
my $profile = $bm_actual->billing_mappings->first->billing_profile;
$bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $start_of_next_interval);
my $profile = $bm_actual->billing_profile;
$interval_unit = $has_package ? $interval_unit : ($profile->interval_unit // _DEFAULT_PROFILE_INTERVAL_UNIT);
$interval_value = $has_package ? $interval_value : ($profile->interval_count // _DEFAULT_PROFILE_INTERVAL_COUNT);
@ -402,8 +403,8 @@ sub set_contract_balance {
$log_vals->{old_package} = ( $package ? { $package->get_inflated_columns } : undef);
$log_vals->{new_package} = $log_vals->{old_package};
$log_vals->{old_balance} = { $balance->get_inflated_columns };
my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
my $profile = $bm_actual->billing_mappings->first->billing_profile;
my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
my $profile = $bm_actual->billing_profile;
$log_vals->{old_profile} = { $profile->get_inflated_columns };
$log_vals->{amount} = $cash_balance - $balance->cash_balance;
}
@ -420,8 +421,8 @@ sub set_contract_balance {
$contract->discard_changes();
if ($log_vals) {
$log_vals->{new_balance} = { $balance->get_inflated_columns };
my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
my $profile = $bm_actual->billing_mappings->first->billing_profile;
my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
my $profile = $bm_actual->billing_profile;
$log_vals->{new_profile} = { $profile->get_inflated_columns };
}
@ -455,8 +456,8 @@ sub topup_contract_balance {
now => $now);
if ($log_vals) {
$log_vals->{old_balance} = { $balance->get_inflated_columns };
my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
my $profile = $bm_actual->billing_mappings->first->billing_profile;
my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
my $profile = $bm_actual->billing_profile;
$log_vals->{old_profile} = { $profile->get_inflated_columns };
if ($subscriber) {
$log_vals->{old_lock_level} = NGCP::Panel::Utils::Subscriber::get_provisoning_voip_subscriber_lock_level(
@ -513,8 +514,8 @@ sub topup_contract_balance {
$contract->discard_changes();
if ($log_vals) {
$log_vals->{new_balance} = { $balance->get_inflated_columns };
my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
my $profile = $bm_actual->billing_mappings->first->billing_profile;
my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
my $profile = $bm_actual->billing_profile;
$log_vals->{new_profile} = { $profile->get_inflated_columns };
}
@ -591,8 +592,8 @@ sub create_initial_contract_balances {
my ($underrun_lock_ts,$underrun_profiles_ts) = (undef,undef);
my ($underrun_lock_applied,$underrun_profiles_applied) = (0,0);
PREPARE_BALANCE_INITIAL:
my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
my $profile = $bm_actual->billing_mappings->first->billing_profile;
my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now);
my $profile = $bm_actual->billing_profile;
if (defined $contract->contact->reseller_id && $package) {
$start_mode = $package->balance_interval_start_mode;
$interval_unit = $package->balance_interval_unit;
@ -678,8 +679,8 @@ sub _get_resized_balance_values {
my $contract = $balance->contract;
my $contract_create = NGCP::Panel::Utils::DateTime::set_local_tz($contract->create_timestamp // $contract->modify_timestamp);
if (NGCP::Panel::Utils::DateTime::set_local_tz($balance->start) <= $contract_create && (NGCP::Panel::Utils::DateTime::is_infinite_future($balance->end) || NGCP::Panel::Utils::DateTime::set_local_tz($balance->end) >= $contract_create)) {
my $bm = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $contract_create); #now => $balance->start); #end); !?
my $profile = $bm->billing_mappings->first->billing_profile;
my $bm = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $contract_create); #now => $balance->start); #end); !?
my $profile = $bm->billing_profile;
my $old_ratio = get_free_ratio($contract_create,NGCP::Panel::Utils::DateTime::set_local_tz($balance->start),NGCP::Panel::Utils::DateTime::set_local_tz($balance->end));
my $old_free_cash = $old_ratio * ($profile->interval_free_cash // _DEFAULT_PROFILE_FREE_CASH);
my $old_free_time = $old_ratio * ($profile->interval_free_time // _DEFAULT_PROFILE_FREE_TIME);
@ -968,17 +969,6 @@ sub get_timely_range {
return ($is_timely,$timely_start,$timely_end);
}
sub get_actual_billing_mapping {
my %params = @_;
my ($c,$schema,$contract,$now) = @params{qw/c schema contract now/};
$schema //= $c->model('DB');
$now //= NGCP::Panel::Utils::DateTime::current_local;
my $contract_create = NGCP::Panel::Utils::DateTime::set_local_tz($contract->create_timestamp // $contract->modify_timestamp);
my $dtf = $schema->storage->datetime_parser;
$now = $contract_create if $now < $contract_create; #if there is no mapping starting with or before $now, it would returns the mapping with max(id):
return $schema->resultset('billing_mappings_actual')->search({ contract_id => $contract->id },{bind => [ ( $dtf->format_datetime($now) ) x 2, ($contract->id) x 2 ],})->first;
}
sub set_subscriber_lock_level {
my %params = @_;
my ($c,$contract,$lock_level) = @params{qw/c contract lock_level/};
@ -1059,10 +1049,10 @@ sub add_profile_mappings {
my ($c,$contract,$bm_actual,$package,$stime,$profiles,$now) = @params{qw/c contract bm_actual package stime profiles now/};
my @profiles;
if ($contract->status ne 'terminated' && (scalar (@profiles = $package->$profiles->all)) > 0) {
$bm_actual //= get_actual_billing_mapping(c => $c,
$bm_actual //= NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c,
contract => $contract,
now => $now);
my $product_id = $bm_actual->billing_mappings->first->product->id;
my $product_id = $bm_actual->product->id;
#my $old_prepaid = $bm_actual->billing_mappings->first->billing_profile->prepaid;
my @mappings_to_create = ();
foreach my $mapping (@profiles) {
@ -1077,13 +1067,13 @@ sub add_profile_mappings {
foreach my $mapping (@mappings_to_create) {
$contract->billing_mappings->create($mapping);
}
$bm_actual = get_actual_billing_mapping(c => $c,
$bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c,
contract => $contract,
now => $now);
NGCP::Panel::Utils::Subscriber::switch_prepaid_contract(c => $c,
#old_prepaid => $old_prepaid,
#new_prepaid => $bm_actual->billing_mappings->first->billing_profile->prepaid,
prepaid => $bm_actual->billing_mappings->first->billing_profile->prepaid,
prepaid => $bm_actual->billing_profile->prepaid,
contract => $contract,
);
return scalar @profiles;
@ -1422,7 +1412,7 @@ sub get_customer_datatable_cols {
return (
{ name => "id", search => 1, title => $c->loc("#") },
{ name => "external_id", search => 1, title => $c->loc("External #") },
#{ name => "billing_mappings_actual.billing_mappings.product.name", search => 1, title => $c->loc("Product") },
#{ name => "product.name", search => 1, title => $c->loc("Product") },
{ name => "contact.email", search => 1, title => $c->loc("Contact Email") },
{ name => "status", search => 1, title => $c->loc("Status") },
);

@ -96,11 +96,16 @@ $(document).ready(function() {
if(data == null)
return '';
[% IF helper.custom_renderers.${f} -%]
data.escapeHtml = true; //true by default;
var renderCustom = [% helper.custom_renderers.${f} -%];
[% ELSE -%]
var renderCustom = function(data, type, full) { return data; };
var renderCustom = function(data, type, full) { data.escapeHtml = true; return data; };
[% END -%]
return String(renderCustom(data, type, full)).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');//"
var str = String(renderCustom(data, type, full));
if (data.escapeHtml) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');//"
}
return str;
},
"bSortable": [% IF helper.column_sortable_map.${f} %] true [% ELSE %] false [% END %]
},
@ -108,7 +113,7 @@ $(document).ready(function() {
{ "mRender": function ( data, type, full ) {
var html = '' +
'<div class="sw_actions pull-right">';
[%
[%
backuri = backuri | uri;
FOR button IN helper.dt_buttons;
separator = '?';
@ -123,7 +128,7 @@ $(document).ready(function() {
IF button.condition;
-%]
if([% button.condition %]) {
[%
[%
END;
-%]
html +=
@ -135,7 +140,7 @@ $(document).ready(function() {
IF button.condition;
-%]
}
[%
[%
END;
END;
-%]

Loading…
Cancel
Save