package NGCP::Panel::Utils::Invoice; use Sipwise::Base; 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; use NGCP::Panel::Utils::CallList; use HTML::Entities; use Geography::Countries qw/country/; use HTTP::Status qw(:constants); sub get_invoice_amounts { my(%params) = @_; my($customer_contract,$billing_profile,$contract_balance,$zonecalls,$category) = @params{qw/customer_contract billing_profile contract_balance zonecalls category/}; my $invoice = {}; $billing_profile->{interval_charge} //= 0.0; $customer_contract->{vat_rate} //= 0.0; if ($zonecalls) { $invoice->{amount_net} = 0.0; map { if ($category eq 'customer' or $category eq 'did') { $invoice->{amount_net} += $_->{customercost}; } elsif ($category eq 'reseller') { $invoice->{amount_net} += $_->{resellercost}; } elsif ($category eq 'peer') { $invoice->{amount_net} += $_->{carriercost}; } } values %$zonecalls; } else { $contract_balance->{cash_balance_interval} //= 0.0; #use Data::Dumper; #print Dumper [$contract_balance,$billing_profile]; $invoice->{amount_net} = ($contract_balance->{cash_balance_interval} + $billing_profile->{interval_charge}) / 100.0; } $invoice->{amount_vat} = $customer_contract->{add_vat} ? $invoice->{amount_net} * ($customer_contract->{vat_rate} / 100.0) : 0.0; $invoice->{amount_total} = $invoice->{amount_net} + $invoice->{amount_vat}; return $invoice; } sub get_invoice_serial{ my($c,$params) = @_; my($invoice) = @$params{qw/invoice/}; return sprintf("INV%04d%02d%07d", $invoice->{period_start}->year, $invoice->{period_start}->month, $invoice->{id}); } sub prepare_contact_data{ my($contact) = @_; $contact->{country} = country($contact->{country} || 0); foreach(keys %$contact){ $contact->{$_} = encode_entities($contact->{$_}, '<>&"'); } #passed by reference #return $contact; } sub create_invoice { my($c,$params) = @_; my($contract,$stime,$etime,$tmpl,$invoice_data) = @$params{qw/contract stime etime tmpl invoice_data/}; my $invoice; my $schema = $c->model('DB'); $schema->set_wait_timeout(1800); #this has to be refactored - select a contract balance instead of a "period" my $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance( c => $c, contract => $contract, stime => $stime, etime => $etime,); unless ($balance) { die { showdetails => $c->loc('period start=[_1] end=[_2].', $stime, $etime), error => 'Could not find active balance.', httpcode => HTTP_UNPROCESSABLE_ENTITY, }; } $stime = NGCP::Panel::Utils::DateTime::convert_tz($balance->start,undef,'UTC',$c); $etime = NGCP::Panel::Utils::DateTime::convert_tz($balance->end,undef,'UTC',$c); my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping( c => $c, contract => $contract, now => $balance->start); my $billing_profile = $bm_actual->billing_profile; my $zonecalls = {}; my $did_zonecalls = []; if ($tmpl->category eq 'did') { foreach my $subs ($schema->resultset('voip_subscribers')->search({ contract_id => $contract->id, #status => { '!=' => 'terminated' }, #'provisioning_voip_subscriber.is_pbx_group' => 0, }, #{ #join => 'provisioning_voip_subscriber', #} )->all) { my $zc = NGCP::Panel::Utils::Contract::get_contract_zonesfees( c => $c, contract_id => $contract->id, stime => $stime, etime => $etime, call_direction => $tmpl->call_direction, group_by_detail => 1, category => $tmpl->category, subscriber_uuid => $subs->uuid, ); my $s = { $subs->get_inflated_columns }; $s->{primary_number} = { $subs->primary_number->get_inflated_columns } if $subs->primary_number; $s->{prov_subscriber} = { $subs->provisioning_voip_subscriber->get_inflated_columns } if $subs->provisioning_voip_subscriber; push(@$did_zonecalls,{ subscriber => $s, zonecalls => $zc, totalcost => 0.0, totalduration => 0.0, }) if scalar keys %$zc; } } $zonecalls = NGCP::Panel::Utils::Contract::get_contract_zonesfees( c => $c, contract_id => $contract->id, stime => $stime, etime => $etime, call_direction => $tmpl->call_direction, group_by_detail => 1, category => $tmpl->category, ); my $calllist = []; if ($tmpl->category eq 'customer') { if (my $calllist_rs = NGCP::Panel::Utils::Contract::get_contract_calls_rs( c => $c, contract_id => $contract->id, stime => $stime, etime => $etime, call_direction => $tmpl->call_direction, category => $tmpl->category, )) { $calllist = [ map { my $call = {$_->get_inflated_columns}; $call->{start_time} = $call->{start_time}->epoch; $call->{destination_user_in} =~s/%23/#/g; #$call->{destination_user_in} = encode_entities($call->{destination_user_in}, '<>&"#'); $call->{cost} += 0.0; # make sure it's a number #$call->{source_customer_cost} += 0.0; # make sure it's a number #$call->{source_reseller_cost} += 0.0 if exists $call->{source_reseller_cost}; #$call->{source_carrier_cost} += 0.0 if exists $call->{source_carrier_cost}; NGCP::Panel::Utils::CallList::suppress_cdr_fields($c,$call,$_); } $calllist_rs->all ]; } } my $invoice_amounts = get_invoice_amounts( customer_contract => {$contract->get_inflated_columns}, #support legacy contract => {$contract->get_inflated_columns}, billing_profile => {$billing_profile->get_inflated_columns}, contract_balance => {$balance->get_inflated_columns}, zonecalls => $zonecalls, category => $tmpl->category, ); @{$invoice_data}{qw/amount_net amount_vat amount_total/} = @$invoice_amounts{qw/amount_net amount_vat amount_total/}; # generate tmp serial here, derive one from after insert $invoice_data->{serial} = "tmp".time.int(rand(99999)); $invoice_data->{data} = undef; #maybe inflation should be applied? Generation failed here, although the latest schema applied. $invoice_data->{period_start} = $stime->ymd.' '. $stime->hms; $invoice_data->{period_end} = $etime->ymd.' '. $etime->hms; try { $invoice = $schema->resultset('invoices')->create($invoice_data); } catch($e) { die { showdetails => $c->loc('Failed to save invoice meta data.'), error => $e, httpcode => HTTP_UNPROCESSABLE_ENTITY, }; } #sprintf("INV%04d%02d%07d", $stime->year, $stime->month, $invoice->id); #to make it unified for web and cron script my $serial = NGCP::Panel::Utils::Invoice::get_invoice_serial($c,{ invoice=>{ period_start => $stime, period_end => $etime, id => $invoice->id, }}); my $svg = $tmpl->data; utf8::decode($svg); my $t = NGCP::Panel::Utils::InvoiceTemplate::get_tt(); my $out = ''; my $pdf = ''; my $vars = {}; # TODO: index 170 seems the upper limit here, then the calllist breaks $vars->{rescontact} = { $contract->contact->reseller->contract->contact->get_inflated_columns } if $contract->contact->reseller; $vars->{customer} = { $contract->get_inflated_columns }; $vars->{contract} = { $contract->get_inflated_columns }; $vars->{custcontact} = { $contract->contact->get_inflated_columns }; $vars->{contact} = { $contract->contact->get_inflated_columns }; $vars->{billprof} = { $billing_profile->get_inflated_columns }; prepare_contact_data($vars->{billprof}); prepare_contact_data($vars->{custcontact}); prepare_contact_data($vars->{contact}); prepare_contact_data($vars->{rescontact}); $vars->{invoice} = { period_start => $stime, period_end => $etime, serial => $serial, amount_net => $invoice_data->{amount_net}, amount_vat => $invoice_data->{amount_vat}, amount_total => $invoice_data->{amount_total}, contract_balance => { $balance->get_inflated_columns }, call_direction => ($tmpl->call_direction eq 'in' ? 'from' : ($tmpl->call_direction eq 'out' ? 'to' : ($tmpl->call_direction eq 'in_out' ? 'from/to' : ''))), }; $vars->{calls} = $calllist; $vars->{zones} = { totalcost => 0.0, totalduration => 0.0, data => [ values(%{ $zonecalls }) ], }; map { if ($tmpl->category eq 'customer' or $tmpl->category eq 'did') { $vars->{zones}->{totalcost} += $_->{customercost}; } elsif ($tmpl->category eq 'reseller') { $vars->{zones}->{totalcost} += $_->{resellercost}; } elsif ($tmpl->category eq 'peer') { $vars->{zones}->{totalcost} += $_->{carriercost}; } $vars->{zones}->{totalduration} += $_->{duration}; } values %$zonecalls; map { my $did_zc = $_; map { if ($tmpl->category eq 'customer' or $tmpl->category eq 'did') { $did_zc->{totalcost} += $_->{customercost}; } elsif ($tmpl->category eq 'reseller') { $did_zc->{totalcost} += $_->{resellercost}; } elsif ($tmpl->category eq 'peer') { $did_zc->{totalcost} += $_->{carriercost}; } $did_zc->{totalduration} += $_->{duration}; $did_zc->{data} = [ values(%{ delete $did_zc->{zonecalls} }) ]; } values %{$did_zc->{zonecalls}}; } @$did_zonecalls; $vars->{did_zones} = $did_zonecalls; #use Data::Dumper; #$c->log->debug(Dumper($did_zonecalls)); $t->process(\$svg, $vars, \$out) || do { 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, httpcode => HTTP_UNPROCESSABLE_ENTITY, }; }; NGCP::Panel::Utils::InvoiceTemplate::preprocess_svg(\$out); NGCP::Panel::Utils::InvoiceTemplate::svg_pdf($c, \$out, \$pdf); $invoice->update({ serial => $serial, data => $pdf, }); NGCP::Panel::Utils::Message::info( c => $c, cname => 'create', log => $vars->{invoice}, desc => $c->loc('Invoice #[_1] successfully created', $invoice->id), ); return $invoice; } sub check_invoice_data{ my ($c,$params) = @_; my ($contract_id,$tmpl_id,$period,$period_start,$period_end) = @$params{qw/contract_id tmpl_id period period_start period_end/}; my $invoice_data = {}; my $schema = $c->model('DB'); my $tmpl = $schema->resultset('invoice_templates')->search({ id => $tmpl_id, }); if ($c->user->roles eq "admin") { } elsif ($c->user->roles eq "reseller") { $tmpl = $tmpl->search({ reseller_id => $c->user->reseller_id, }); } $tmpl = $tmpl->first; unless ($tmpl) { 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", httpcode => HTTP_UNPROCESSABLE_ENTITY, }; } my $contract; if ('customer' eq $tmpl->category or 'did' eq $tmpl->category) { $contract = NGCP::Panel::Utils::Contract::get_customer_rs(c => $c)->find({ 'me.id' => $contract_id }); unless($contract) { die { showdetails => $c->loc('Customer not found'), error => "invalid contract_id $contract_id", httpcode => HTTP_UNPROCESSABLE_ENTITY, }; } unless($contract->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", httpcode => HTTP_UNPROCESSABLE_ENTITY, }; } } else { my @product_ids = map { $_->id; } $schema->resultset('products')->search_rs({ 'class' => ['pstnpeering','sippeering','reseller'] })->all; $contract = NGCP::Panel::Utils::Contract::get_contract_rs(c => $c)->search_rs({ 'me.id' => $contract_id, 'product_id' => { -in => [ @product_ids ] }, },{ join => 'contact', })->first; unless($contract) { die { showdetails => $c->loc('Contract not found'), error => "invalid contract_id $contract_id", httpcode => HTTP_UNPROCESSABLE_ENTITY, }; } } $invoice_data->{contract_id} = $contract_id; #$invoice_data->{category} = $tmpl->category; my $stime = $period_start ? NGCP::Panel::Utils::DateTime::from_string( $period_start ) : NGCP::Panel::Utils::DateTime::from_string( $period )->truncate(to => 'month'); my $etime = $period_end ? NGCP::Panel::Utils::DateTime::from_string( $period_end ) : $stime->clone->add(months => 1)->subtract(seconds => 1); return ($contract,$tmpl,$stime,$etime,$invoice_data); } 1;