Compare commits

..

No commits in common. "b6236822ea1834c200a926f27c45c4a48d37bbf6" and "5f10175b3523029c2307c44f5058e0a537a6a7c3" have entirely different histories.

38 changed files with 666 additions and 588 deletions

View File

@ -3,8 +3,9 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use App\Models\Service; use App\Models\{Service,Site};
class ServiceList extends Command class ServiceList extends Command
{ {
@ -32,7 +33,7 @@ class ServiceList extends Command
*/ */
public function handle() public function handle()
{ {
$header = '|%5s|%-9s|%-30s|%-30s|%7s|%7s|%10s|%10s|%10s|%10s|%10s|'; $header = '|%13s|%-14s|%-35s|%-40s|%8s|%17s|%12s|%12s|%12s|%12s|%14s|';
$this->warn(sprintf($header, $this->warn(sprintf($header,
'ID', 'ID',
@ -41,42 +42,41 @@ class ServiceList extends Command
'Name', 'Name',
'Active', 'Active',
'Status', 'Status',
'Start', 'Next Invoice',
'Stop', 'Start Date',
'Connect', 'Stop Date',
'First', 'Connect Date',
'Next', 'First Invoice'
)); ));
foreach (Service::cursor() as $o) { foreach (Service::withoutGlobalScope(\App\Models\Scopes\SiteScope::class)->with(['site'])->cursor() as $o) {
if ((! $this->option('inactive')) && (! $o->isActive())) if ((! $this->option('inactive')) AND ! $o->isActive())
continue; continue;
if ($this->option('type') && ($o->product->getCategoryAttribute() !== $this->option('type'))) Config::set('site',$o->site);
if ($this->option('type') AND ($o->product->getCategoryAttribute() !== $this->option('type')))
continue; continue;
$c = $o->invoiced_items $c = $o->invoice_items->filter(function($item) {return $item->item_type === 0; })->sortby('start_at')->first();
->filter(fn($item)=>$item->item_type === 0)
->sortby('start_at')
->first();
if ($this->option('fix') && (! $o->start_at) && $c && $c->start_at && $o->type && $o->type->connect_at && $c->start_at->format('Y-m-d') == $o->type->connect_at->format('Y-m-d')) { if ($this->option('fix') AND ! $o->start_at AND $c AND $c->start_at AND $o->type AND $o->type->connect_at AND $c->start_at->format('Y-m-d') == $o->type->connect_at->format('Y-m-d')) {
$o->start_at = $o->type->connect_at; $o->start_at = $o->type->connect_at;
$o->save(); $o->save();
} }
$this->info(sprintf($header, $this->info(sprintf($header,
$o->lid, $o->sid,
$o->product->getCategoryNameAttribute(), $o->product->getCategoryNameAttribute(),
substr($o->product->getNameAttribute(),0,35), substr($o->product->getNameAttribute(),0,35),
substr($o->name_short,0,40), substr($o->name_short,0,40),
$o->active ? 'active' : 'inactive', $o->active ? 'active' : 'inactive',
$o->status, $o->status,
$o->invoice_next?->format('Y-m-d'),
$o->start_at?->format('Y-m-d'), $o->start_at?->format('Y-m-d'),
$o->stop_at?->format('Y-m-d'), $o->stop_at?->format('Y-m-d'),
($o->type && $o->type->connect_at) ? $o->type->connect_at->format('Y-m-d') : NULL, ($o->type AND $o->type->connect_at) ? $o->type->connect_at->format('Y-m-d') : NULL,
($c && $c->start_at) ? $c->start_at->format('Y-m-d') : NULL, ($c && $c->date_start) ? $c->date_start->format('Y-m-d') : NULL,
$o->invoice_next?->format('Y-m-d'),
)); ));
} }
} }

View File

@ -37,8 +37,7 @@ class ChargeController extends Controller
public function delete(Charge $o): array public function delete(Charge $o): array
{ {
if (Gate::allows('delete',$o)) { if (Gate::allows('delete',$o)) {
$o->active = FALSE; $o->delete();
$o->save();
return ['ok']; return ['ok'];

View File

@ -68,11 +68,9 @@ class SearchController extends Controller
foreach (Service::Search($request->input('term')) foreach (Service::Search($request->input('term'))
->whereIN('account_id',$account_ids) ->whereIN('account_id',$account_ids)
->orderBy('id') ->orderBy('id')
->limit(20) ->limit(20)->get() as $o)
->with(['product'])
->get() as $o)
{ {
$result->push(['name'=>sprintf('%s (%s) %s',$o->name,$o->lid,$o->active ? '' : '<small>INACT</small>'),'value'=>'/u/service/'.$o->id,'category'=>$o->product->category_name]); $result->push(['name'=>sprintf('%s (%s) %s',$o->name,$o->lid,$o->active ? '' : '<small>INACT</small>'),'value'=>'/u/service/'.$o->id,'category'=>$o->category_name]);
} }
// Look for an Invoice // Look for an Invoice
@ -95,7 +93,7 @@ class SearchController extends Controller
} }
return $result return $result
->sortBy(fn($item)=>$item['category'].$item['name']) ->sortBy(function($item) { return $item['category'].$item['name']; })
->values(); ->values();
} }
} }

View File

@ -333,7 +333,7 @@ class ServiceController extends Controller
$start_at = Carbon::create(Arr::get($request->broadband,'start_at')); $start_at = Carbon::create(Arr::get($request->broadband,'start_at'));
// Get the invoiced items covering the start_at date // Get the invoiced items covering the start_at date
foreach ($o->invoiced_items->filter(function($item) use ($start_at) { foreach ($o->invoice_items->filter(function($item) use ($start_at) {
return ($item->start_at < $start_at) && ($item->stop_at > $start_at) && ($item->item_type === 0); return ($item->start_at < $start_at) && ($item->stop_at > $start_at) && ($item->item_type === 0);
}) as $iio) }) as $iio)
{ {
@ -351,7 +351,7 @@ class ServiceController extends Controller
$co->stop_at = $iio->stop_at; $co->stop_at = $iio->stop_at;
$co->amount = $iio->price_base; $co->amount = $iio->price_base;
$co->taxable = TRUE; // @todo this should be determined $co->taxable = TRUE; // @todo this should be determined
$co->quantity = -1*$start_at->diffInDays($iio->stop_at)/$iio->start_at->diffInDays($iio->stop_at); $co->quantity = -1*$start_at->diff($iio->stop_at)->days/$iio->start_at->diff($iio->stop_at)->days;
$charges->push($co); $charges->push($co);
// Add the new charge // Add the new charge
@ -368,7 +368,7 @@ class ServiceController extends Controller
$co->stop_at = $iio->stop_at; $co->stop_at = $iio->stop_at;
$co->amount = Arr::get($request->broadband,'price') ?: $po->base_charge; $co->amount = Arr::get($request->broadband,'price') ?: $po->base_charge;
$co->taxable = TRUE; // @todo this should be determined $co->taxable = TRUE; // @todo this should be determined
$co->quantity = $start_at->diffInDays($iio->stop_at)/$iio->start_at->diffInDays($iio->stop_at); $co->quantity = $start_at->diff($iio->stop_at)->days/$iio->start_at->diff($iio->stop_at)->days;
$charges->push($co); $charges->push($co);
} }
@ -424,10 +424,10 @@ class ServiceController extends Controller
$request->post(), $request->post(),
$x=collect($o->type->validation()) $x=collect($o->type->validation())
->keys() ->keys()
->transform(fn($item)=>sprintf('%s.%s',$o->product->category,$item)) ->transform(fn($item)=>sprintf('%s.%s',$o->category,$item))
->combine(array_values($o->type->validation())) ->combine(array_values($o->type->validation()))
->transform(fn($item)=>is_string($item) ->transform(fn($item)=>is_string($item)
? preg_replace('/^exclude_without:/',sprintf('exclude_without:%s.',$o->product->category),$item) ? preg_replace('/^exclude_without:/',sprintf('exclude_without:%s.',$o->category),$item)
: $item) : $item)
->merge( ->merge(
[ [
@ -436,7 +436,7 @@ class ServiceController extends Controller
'recur_schedule' => ['required',Rule::in(collect(Invoice::billing_periods)->keys())], 'recur_schedule' => ['required',Rule::in(collect(Invoice::billing_periods)->keys())],
'invoice_next_at' => 'nullable|date', 'invoice_next_at' => 'nullable|date',
'price' => 'nullable|numeric', 'price' => 'nullable|numeric',
$o->product->category => 'array|min:1', $o->category => 'array|min:1',
] ]
) )
->toArray() ->toArray()
@ -452,13 +452,13 @@ class ServiceController extends Controller
$validated = collect($validator->validated()); $validated = collect($validator->validated());
// Store our service type values // Store our service type values
$o->type->forceFill($validated->get($o->product->category)); $o->type->forceFill($validated->get($o->category));
// Some special handling // Some special handling
switch ($o->product->category) { switch ($o->category) {
case 'broadband': case 'broadband':
// If pppoe is not set, then we dont need username/password // If pppoe is not set, then we dont need username/password
$o->type->pppoe = ($x=data_get($validated,$o->product->category.'.pppoe',FALSE)); $o->type->pppoe = ($x=data_get($validated,$o->category.'.pppoe',FALSE));
if (! $x) { if (! $x) {
$o->type->service_username = NULL; $o->type->service_username = NULL;
@ -487,7 +487,7 @@ class ServiceController extends Controller
else { else {
// For broadband, start_at is connect_at in the type record // For broadband, start_at is connect_at in the type record
switch ($o->product->category) { switch ($o->category) {
case 'broadband': case 'broadband':
$o->start_at = $o->type->connect_at; $o->start_at = $o->type->connect_at;
break; break;

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Models\{Account,Invoice};
class AccountController extends Controller
{
/**
* Show the users next invoice
*/
public function view_invoice_next(Account $o)
{
$io = new Invoice;
$io->account = $o;
// Get the account services
$s = $o->services(TRUE)
->with(['invoice_items','charges'])
->get()
->filter(function($item) {
return ! $item->suspend_billing AND ! $item->external_billing;
});
// Get our invoice due date for this invoice
$io->due_at = $s->min(function($item) { return $item->invoice_next; });
// @todo The days in advance is an application parameter
$io->created_at = $io->due_at->subDays(30);
// Work out items to add to this invoice, plus any in the next additional days
$days = now()->diffInDays($io->due_at)+1+7;
foreach ($s as $so)
{
if ($so->isInvoiceDueSoon($days))
foreach ($so->next_invoice_items() as $o)
$io->items->push($o);
}
return view('theme.backend.adminlte.u.invoice.home')
->with('o',$io);
}
}

View File

@ -15,4 +15,9 @@ class ReportController extends Controller
{ {
return view('product/report'); return view('product/report');
} }
public function services()
{
return view('service/report');
}
} }

View File

@ -131,7 +131,7 @@ class ImportCosts implements ShouldQueue
if ($so) { if ($so) {
// r[1] = Monthly Charge or Extra Charge,r[2] = "On Plan", r[3] = Plan Info // r[1] = Monthly Charge or Extra Charge,r[2] = "On Plan", r[3] = Plan Info
$r = []; $r = [];
switch ($so->product->category) { switch ($so->category) {
case 'broadband': case 'broadband':
$to = Cost\Broadband::where('site_id',$this->co->site_id) $to = Cost\Broadband::where('site_id',$this->co->site_id)
->where('cost_id',$this->co->id) ->where('cost_id',$this->co->id)
@ -192,8 +192,8 @@ class ImportCosts implements ShouldQueue
break; break;
default: default:
dump(['so'=>$so,'category'=>$so->product->category,'line'=>$line,'m'=>$m,'r'=>$r]); dump(['so'=>$so,'category'=>$so->category,'line'=>$line,'m'=>$m,'r'=>$r]);
throw new \Exception(sprintf('ERROR: Service type not handled for service [%s] (%s) on line [%d]',$m[1],$so->product->category,$c)); throw new \Exception(sprintf('ERROR: Service type not handled for service [%s] (%s) on line [%d]',$m[1],$so->category,$c));
} }
} else { } else {

View File

@ -14,7 +14,7 @@ class OrderRequestApprove extends Mailable
{ {
use Queueable, SerializesModels; use Queueable, SerializesModels;
public Service $so; public Service $service;
public string $notes; public string $notes;
/** /**
@ -25,7 +25,7 @@ class OrderRequestApprove extends Mailable
*/ */
public function __construct(Service $o,string $notes='') public function __construct(Service $o,string $notes='')
{ {
$this->so = $o; $this->service = $o;
$this->notes = $notes; $this->notes = $notes;
} }
@ -36,14 +36,14 @@ class OrderRequestApprove extends Mailable
*/ */
public function build() public function build()
{ {
Config::set('site',$this->so->site); Config::set('site',$this->service->site);
// @todo This is not consistent with Cancel/Change Request // @todo This is not consistent with Cancel/Change Request
switch ($this->so->product->category) { switch ($this->service->category) {
case 'broadband': $subject = sprintf('%s: %s',$this->so->product->category,$this->so->type->service_address); case 'broadband': $subject = sprintf('%s: %s',$this->service->category,$this->service->type->service_address);
break; break;
case 'phone': $subject = sprintf('%s: %s',$this->so->product->category,$this->so->type->service_number); case 'phone': $subject = sprintf('%s: %s',$this->service->category,$this->service->type->service_number);
break; break;
default: default:
@ -53,6 +53,6 @@ class OrderRequestApprove extends Mailable
return $this return $this
->markdown('email.admin.order.approve') ->markdown('email.admin.order.approve')
->subject($subject) ->subject($subject)
->with(['site'=>$this->so->site]); ->with(['site'=>$this->service->site]);
} }
} }

View File

@ -2,7 +2,6 @@
namespace App\Models; namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -157,7 +156,7 @@ class Account extends Model implements IDs
public function services_active() public function services_active()
{ {
return $this->services() return $this->services()
->ServiceActive(); ->active();
} }
/** /**
@ -253,59 +252,11 @@ class Account extends Model implements IDs
/* METHODS */ /* METHODS */
public function invoice_next(): Collection
{
// Collect all the invoice items for our active services
$nextdate = ($x=$this
->services_active
->filter(fn($item)=>$item->isBilled() && $item->invoice_next)
->sortBy(fn($item)=>(string)$item->invoice_next))
->first()
->invoice_next
->clone();
// Add any additional items that will be invoiced 30 days
$nextitemsdate = max($nextdate,Carbon::now())
->clone()
->addMonth()
->subDay()
->endOfday();
$items = $x
->filter(fn($item)=>$item->invoice_next->lessThan($nextitemsdate))
->sortBy(fn($item)=>$item->invoice_next.$item->name)
->map(fn($item)=>$item->next_invoice_items($nextitemsdate))
->flatten();
// Add any account charges (charges with no active service)
foreach ($this->charges->filter(function($item) { return $item->unprocessed && ((! $this->service_id) || (! $item->service->isBilled())); }) as $oo) {
$ii = new InvoiceItem;
$ii->active = TRUE;
$ii->service_id = $oo->service_id;
$ii->product_id = $this->product_id;
$ii->quantity = $oo->quantity;
$ii->item_type = $oo->type;
$ii->price_base = $oo->amount;
$ii->start_at = $oo->start_at;
$ii->stop_at = $oo->stop_at;
$ii->module_id = 30; // @todo This shouldnt be hard coded
$ii->module_ref = $oo->id;
$ii->site_id = 1; // @todo
$ii->addTaxes($this->country->taxes);
$items->push($ii);
}
return $items;
}
/** /**
* List of invoices (summary) for this account * List of invoices (summary) for this account
* *
* @param Collection|NULL $invoices * @param Collection|NULL $invoices
* @param bool $all * @return Collection
* @return Builder
*/ */
public function invoiceSummary(Collection $invoices=NULL,bool $all=FALSE): Builder public function invoiceSummary(Collection $invoices=NULL,bool $all=FALSE): Builder
{ {

View File

@ -158,148 +158,6 @@ class Invoice extends Model implements IDs
return ceil(($contract_term ?: 1)/(Arr::get(self::billing_periods,$source.'.interval') ?: 1)); return ceil(($contract_term ?: 1)/(Arr::get(self::billing_periods,$source.'.interval') ?: 1));
} }
/**
* Work out the time period for a particular date and invoice period
*
* @param \Leenooks\Carbon $date
* @param int $interval
* @param bool $strict
* @return Collection
* @throws \Exception
*/
public static function invoice_period(Carbon $date,int $interval,bool $strict): Collection
{
$date_start = $date->clone();
$date_end = $date->clone();
switch ($interval) {
case self::BILL_WEEKLY:
$result = collect([
'start' => $strict
? $date_start->startOfWeek()
: $date_start,
'end'=> $strict
? $date_end->endOfWeek()
: $date_end->addWeek()->subDay()
]);
break;
case self::BILL_MONTHLY:
$result = collect([
'start' => $strict
? $date_start->startOfMonth()
: $date_start,
'end' => $strict
? $date_end->endOfMonth()
: $date_end->addMonth()->subDay()
]);
break;
case self::BILL_QUARTERLY:
$result = collect([
'start' => $strict// The service charges
? $date_start->startOfQuarter()
: $date_start,
'end' => $strict
? $date_end->endOfQuarter()
: $date_end->addQuarter()->subDay()
]);
break;
case self::BILL_SEMI_YEARLY:
$result = collect([
'start' => $strict
? $date_start->startOfHalf()
: $date_start,
'end' => $strict
? $date_end->endOfHalf()
: $date_end->addQuarters(2)->subDay()
]);
break;
case self::BILL_YEARLY:
$result = collect([
'start' => $strict
? $date_start->startOfYear()
: $date_start,
'end' => $strict
? $date_end->endOfYear()
: $date_end->addYear()->subDay()
]);
break;
case self::BILL_TWOYEARS:
if (! $strict) {
$result = collect([
'start' => $date_start,
'end' => $date_end->addYears(2)->subDay(),
]);
} else {
$data_end = $date_end->addYears(2)->subDay()->endOfYear();
// Make sure we end on an even year
if ($data_end->clone()->addDay()->year%2)
$data_end = $data_end->subYear();
$result = collect([
'start' => $data_end->clone()->subYears(2)->addDay(),
'end' => $data_end,
]);
}
break;
// NOTE: price_recur_strict ignored
case self::BILL_THREEYEARS:
$result = collect([
'start' => $date_start,
'end' => $date_end->addYears(3)->subDay(),
]);
break;
// NOTE: price_recur_strict ignored
case self::BILL_FOURYEARS:
$result = collect([
'start' => $date_start,
'end' => $date_end->addYears(4)->subDay(),
]);
break;
// NOTE: price_recur_strict ignored
case self::BILL_FIVEYEARS:
$result = collect([
'start' => $date_start,
'end' => $date_end->addYears(5)->subDay(),
]);
break;
default:
throw new \Exception('Unknown recur_schedule: '.$interval);
}
return $result;
}
/**
* @param \Leenooks\Carbon $start Start Date
* @param Carbon $end End Date
* @param int $interval Period End Date
* @param bool $strict
* @return float
* @throws \Exception
*/
public static function invoice_quantity(Carbon $start,Carbon $end,Collection $period): float
{
if ($start->lessThan(Arr::get($period,'start')) || $end->greaterThan(Arr::get($period,'end')))
throw new \Exception('Billing Period differ');
$d = Arr::get($period,'start')->diffInDays(Arr::get($period,'end'));
if (! $d)
throw new \Exception('Start and End period dates cannot be the same');
return round(($d-Arr::get($period,'start')->diffInDays($start)-$end->diffInDays(Arr::get($period,'end')))/$d,2);
}
/* INTERFACES */ /* INTERFACES */
/** /**

View File

@ -30,16 +30,13 @@ class InvoiceItem extends Model
// Array of items that can be updated with PushNew // Array of items that can be updated with PushNew
protected $pushable = ['taxes']; protected $pushable = ['taxes'];
public const INVOICEITEM_SERVICE = 0;
public const INVOICEITEM_SETUP = 4;
// @todo Change these to CONSTS so it's easier to reference through out the code // @todo Change these to CONSTS so it's easier to reference through out the code
public const type = [ public const type = [
self::INVOICEITEM_SERVICE => 'Service Charge', 0 => 'Service Charge',
1 => 'Hardware', // * 1 => 'Hardware', // *
2 => 'Service Relocation Fee', // * Must have corresponding SERVICE_ID 2 => 'Service Relocation Fee', // * Must have corresponding SERVICE_ID
3 => 'Service Change', // * Must have corresponding SERVICE_ID 3 => 'Service Change', // * Must have corresponding SERVICE_ID
self::INVOICEITEM_SETUP => 'Service Connection', // * Must have corresponding SERVICE_ID 4 => 'Service Connection', // * Must have corresponding SERVICE_ID
6 => 'Service Cancellation', // * Must have corresponding SERVICE_ID 6 => 'Service Cancellation', // * Must have corresponding SERVICE_ID
7 => 'Extra Product/Service Charge', // * Service Billing in advance, Must have corresponding SERVICE_ID 7 => 'Extra Product/Service Charge', // * Service Billing in advance, Must have corresponding SERVICE_ID
8 => 'Product Addition', // * Additional Product Customisation, Must have corresponding SERVICE_ID 8 => 'Product Addition', // * Additional Product Customisation, Must have corresponding SERVICE_ID

View File

@ -2,7 +2,6 @@
namespace App\Models; namespace App\Models;
use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Casts\AsCollection; use Illuminate\Database\Eloquent\Casts\AsCollection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -12,11 +11,12 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Leenooks\Carbon;
use Leenooks\Casts\LeenooksCarbon; use Leenooks\Casts\LeenooksCarbon;
use App\Models\Product\Type; use App\Models\Product\Type;
use App\Interfaces\IDs; use App\Interfaces\IDs;
use App\Traits\{ScopeServiceActive,ScopeServiceUserAuthorised}; use App\Traits\ScopeServiceUserAuthorised;
/** /**
* Class Service * Class Service
@ -29,10 +29,12 @@ use App\Traits\{ScopeServiceActive,ScopeServiceUserAuthorised};
* *
* Attributes for services: * Attributes for services:
* + additional_cost : Pending additional charges for this service (excluding setup) //@todo check all these are still valid * + additional_cost : Pending additional charges for this service (excluding setup) //@todo check all these are still valid
* + billing_charge : Charge for this service each invoice period * + billing_charge : Charge for this service each invoice period // @todo change to "charge"
* + billing_interval : The period that this service is billed for by default * + billing_interval : The period that this service is billed for by default
* + billing_interval_string : The period that this service is billed for by default as a name * + billing_interval_string : The period that this service is billed for by default as a name
* + invoiced_to : When this service has been billed to * + billed_to : When this service has been billed to // @todo rename all references to invoice_to
* + category : The type of service this is, eg: broadband, phone
* + category_name : The type of service this is, eg: Broadband, Telephone (in human friendly)
* + contract_term : The term that this service must be active * + contract_term : The term that this service must be active
* + contract_end : The date that the contract ends for this service * + contract_end : The date that the contract ends for this service
* + name : Service short name with service address * + name : Service short name with service address
@ -52,7 +54,7 @@ use App\Traits\{ScopeServiceActive,ScopeServiceUserAuthorised};
*/ */
class Service extends Model implements IDs class Service extends Model implements IDs
{ {
use HasFactory,ScopeServiceActive,ScopeServiceUserAuthorised; use HasFactory,ScopeServiceUserAuthorised;
protected $casts = [ protected $casts = [
'order_info' => AsCollection::class, 'order_info' => AsCollection::class,
@ -279,8 +281,8 @@ class Service extends Model implements IDs
public static function movements(User $uo): Collection public static function movements(User $uo): Collection
{ {
return (new self) return (new self)
->ServiceActive() ->active()
->ServiceUserAuthorised($uo) ->serviceUserAuthorised($uo)
->where('order_status','!=','ACTIVE') ->where('order_status','!=','ACTIVE')
->with(['account','product']) ->with(['account','product'])
->get(); ->get();
@ -341,7 +343,7 @@ class Service extends Model implements IDs
public function charges_active() public function charges_active()
{ {
return $this->charges() return $this->charges()
->ServiceActive(); ->active();
} }
/** /**
@ -369,7 +371,6 @@ class Service extends Model implements IDs
*/ */
public function invoice_items($active=TRUE) public function invoice_items($active=TRUE)
{ {
Log::alert('Call to deprecated functon '.__METHOD__);
return $this->invoiced_items_active(); return $this->invoiced_items_active();
} }
@ -417,7 +418,6 @@ class Service extends Model implements IDs
{ {
return $this->hasMany(InvoiceItem::class) return $this->hasMany(InvoiceItem::class)
->where('item_type','=',0) ->where('item_type','=',0)
->whereNotNull('start_at')
->orderBy('start_at','desc'); ->orderBy('start_at','desc');
} }
@ -464,11 +464,9 @@ class Service extends Model implements IDs
/** /**
* Only query active categories * Only query active categories
* @deprecated use ScopeServiceActive
*/ */
public function scopeActive($query) public function scopeActive($query)
{ {
throw new \Exception('deprecated');
return $query->where( return $query->where(
fn($query)=> fn($query)=>
$query->where($this->getTable().'.active',TRUE) $query->where($this->getTable().'.active',TRUE)
@ -481,11 +479,9 @@ class Service extends Model implements IDs
* *
* @param $query * @param $query
* @return mixed * @return mixed
* @deprecated use ScopeServiceInactive
*/ */
public function scopeInactive($query) public function scopeInActive($query)
{ {
dd('deprecated');
return $query->where( return $query->where(
fn($query)=> fn($query)=>
$query->where($this->getTable().'.active',FALSE) $query->where($this->getTable().'.active',FALSE)
@ -531,7 +527,15 @@ class Service extends Model implements IDs
*/ */
public function getBillingChargeAttribute(): float public function getBillingChargeAttribute(): float
{ {
return $this->account->taxed($this->billing_charge()); // If recur_schedule is null, then we only bill this item once
if (is_null($this->recur_schedule) && $this->getInvoiceToAttribute())
$this->price = 0;
return $this->account->taxed(
is_null($this->price)
? $this->product->getBaseChargeAttribute($this->recur_schedule,$this->account->group)
: $this->price
);
} }
/** /**
@ -542,7 +546,7 @@ class Service extends Model implements IDs
*/ */
public function getBillingChargeNormalisedAttribute(): float public function getBillingChargeNormalisedAttribute(): float
{ {
return number_format($this->getBillingChargeAttribute()*Invoice::billing_change($this->getBillingIntervalAttribute(),$this->offering->billing_interval),2); return number_format($this->getBillingChargeAttribute()*Invoice::billing_change($this->recur_schedule,$this->offering->billing_interval),2);
} }
/** /**
@ -565,6 +569,39 @@ class Service extends Model implements IDs
return Invoice::billing_name($this->getBillingIntervalAttribute()); return Invoice::billing_name($this->getBillingIntervalAttribute());
} }
/**
* Determine a monthly price for a service, even if it is billed at a different frequency
*
* @return float
* @throws Exception
* @deprecated use class::billing_charge_normalised()
*/
public function getBillingMonthlyPriceAttribute(): float
{
Log::alert('SMO:! Deprecated function getBillingMonthlyPriceAttribute()');
return $this->getBillingChargeNormalisedAttribute();
}
/**
* Service Category ID
*
* @return string
*/
public function getCategoryAttribute(): string
{
return $this->product->category;
}
/**
* Service Category Name
*
* @return string
*/
public function getCategoryNameAttribute(): string
{
return $this->product->category_name;
}
/** /**
* The date the contract ends * The date the contract ends
* *
@ -613,24 +650,157 @@ class Service extends Model implements IDs
* *
* @return Carbon * @return Carbon
*/ */
public function getInvoiceNextAttribute(): ?Carbon public function getInvoiceNextAttribute(): Carbon
{ {
$last = $this->getInvoicedToAttribute(); $last = $this->getInvoiceToAttribute();
if ($this->stop_at && $last->greaterThan($this->stop_at))
return NULL;
return $last return $last
? $last->addDay() ? $last->addDay()
: (min($this->start_at,$this->invoice_next_at) ?: Carbon::now()); : (min($this->start_at,$this->invoice_next_at) ?: Carbon::now());
} }
/**
* Return the end date for the next invoice
*
* @return Carbon
* @throws Exception
*/
public function getInvoiceNextEndAttribute(): Carbon
{
switch ($this->recur_schedule) {
case Invoice::BILL_WEEKLY:
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfWeek()
: $this->getInvoiceNextAttribute()->addWeek()->subDay();
break;
case Invoice::BILL_MONTHLY:
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfMonth()
: $this->getInvoiceNextAttribute()->addMonth()->subDay();
break;
case Invoice::BILL_QUARTERLY:
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfQuarter()
: $this->getInvoiceNextAttribute()->addQuarter()->subDay();
break;
case Invoice::BILL_SEMI_YEARLY:
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfHalf()
: $this->getInvoiceNextAttribute()->addQuarters(2)->subDay();
break;
case Invoice::BILL_YEARLY:
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfYear()
: $this->getInvoiceNextAttribute()->addYear()->subDay();
break;
case Invoice::BILL_TWOYEARS:
if (! $this->product->price_recur_strict) {
$date = $this->getInvoiceNextAttribute()->addYears(2)->subDay();
} else {
$date = $this->getInvoiceNextAttribute()->addYears(2)->subDay()->endOfYear();
// Make sure we end on an even year
if ($date->clone()->addDay()->year%2)
$date = $date->subYear();
}
break;
// NOTE: price_recur_strict ignored
case Invoice::BILL_THREEYEARS:
$date = $this->getInvoiceNextAttribute()->addYears(3)->subDay();
break;
// NOTE: price_recur_strict ignored
case Invoice::BILL_FOURYEARS:
$date = $this->getInvoiceNextAttribute()->addYears(4)->subDay();
break;
// NOTE: price_recur_strict ignored
case Invoice::BILL_FIVEYEARS:
$date = $this->getInvoiceNextAttribute()->addYears(5)->subDay();
break;
default:
throw new Exception('Unknown recur_schedule');
}
// If the invoice has an end date, our invoice period shouldnt be greater than that (might be terminating).
if ($this->stop_at && ($this->stop_at < $date))
$date = $this->stop_at;
return $date;
}
/**
* Determine how much quantity (at the charge rate) is required for the next invoice
*
* @return float
* @throws Exception
*/
public function getInvoiceNextQuantityAttribute(): float
{
// If we are not rounding to the first day of the cycle, then it is always a full cycle
if (! $this->product->price_recur_strict)
return 1;
$n = $this->invoice_next->diffInDays($this->invoice_next_end);
switch ($this->recur_schedule) {
case Invoice::BILL_WEEKLY:
$d = $this->invoice_next_end->addWeek()->startOfWeek()->diffInDays($this->invoice_next->startOfWeek());
break;
case Invoice::BILL_MONTHLY:
$d = $this->invoice_next_end->addMonth()->startOfMonth()->diffInDays($this->invoice_next->startOfMonth());
break;
case Invoice::BILL_QUARTERLY:
$d = $this->invoice_next_end->startOfQuarter()->diffInDays($this->invoice_next->addQuarter()->startOfQuarter());
break;
case Invoice::BILL_SEMI_YEARLY:
$d = $this->invoice_next_end->addQuarter(2)->startOfHalf()->diffInDays($this->invoice_next->startOfHalf());
break;
case Invoice::BILL_YEARLY:
$d = $this->invoice_next_end->addYear()->startOfYear()->diffInDays($this->invoice_next->startOfYear());
break;
case Invoice::BILL_TWOYEARS:
$d = $this->invoice_next_end->addYear(2)->startOfYear()->diffInDays($this->invoice_next->subyear(2))-1;
break;
case Invoice::BILL_THREEYEARS:
$d = $this->invoice_next_end->addYear(3)->startOfYear()->diffInDays($this->invoice_next->subyear(3))-1;
break;
case Invoice::BILL_FOURYEARS:
$d = $this->invoice_next_end->addYear(3)->startOfYear()->diffInDays($this->invoice_next->subyear(4))-1;
break;
case Invoice::BILL_FIVEYEARS:
$d = $this->invoice_next_end->addYear(3)->startOfYear()->diffInDays($this->invoice_next->subyear(5))-1;
break;
default:
throw new Exception('Unknown recur_schedule');
}
return round($n/$d,2);
}
/** /**
* Get the date that the service has been invoiced to * Get the date that the service has been invoiced to
* *
* @return Carbon|null * @return Carbon|null
*/ */
public function getInvoicedToAttribute(): ?Carbon public function getInvoiceToAttribute(): ?Carbon
{ {
return ($x=$this->invoiced_service_items_active_recent)->count() return ($x=$this->invoiced_service_items_active_recent)->count()
? $x->first()->stop_at ? $x->first()->stop_at
@ -656,9 +826,7 @@ class Service extends Model implements IDs
*/ */
public function getNameShortAttribute() public function getNameShortAttribute()
{ {
return $this->type->getServiceNameAttribute() return $this->type->getServiceNameAttribute() ? $this->type->getServiceNameAttribute() : 'SID:'.$this->sid;
? $this->type->getServiceNameAttribute()
: 'SID:'.$this->sid;
} }
/** /**
@ -673,9 +841,7 @@ class Service extends Model implements IDs
*/ */
public function getNameDetailAttribute() public function getNameDetailAttribute()
{ {
return ($this->type->getServiceDescriptionAttribute() !== NULL) return ($this->type->getServiceDescriptionAttribute() !== NULL) ? $this->type->getServiceDescriptionAttribute() : 'No Description';
? $this->type->getServiceDescriptionAttribute()
: 'No Description';
} }
/** /**
@ -712,13 +878,22 @@ class Service extends Model implements IDs
->last(); ->last();
return $lastpaid return $lastpaid
? $this->invoiced_service_items_active ? $this->invoiced_service_items_active->where('invoice_id',$lastpaid->id)->where('type',0)->max('stop_at')
->where('invoice_id',$lastpaid->id)
->where('type',0)
->max('stop_at')
: NULL; : NULL;
} }
/**
* Return the billing recurring configuration for this service
*
* @param $value
* @return int
*/
public function xgetRecurScheduleAttribute($value): int
{
// If recur_schedule not set, default to quarterly
return $value ?? Invoice::BILL_QUARTERLY;
}
/** /**
* Return the Service Status * Return the Service Status
* *
@ -858,7 +1033,6 @@ class Service extends Model implements IDs
public function actions(): Collection public function actions(): Collection
{ {
$next = $this->getStageParameters($this->order_status)->get('next'); $next = $this->getStageParameters($this->order_status)->get('next');
return $next return $next
? $next->map(function($item,$key) { ? $next->map(function($item,$key) {
$authorized = FALSE; $authorized = FALSE;
@ -876,22 +1050,6 @@ class Service extends Model implements IDs
: collect(); : collect();
} }
/**
* This service billing charge, pre-taxes
*
* @return float
*/
public function billing_charge(): float
{
// If recur_schedule is null, then we only bill this item once
if (is_null($this->getBillingIntervalAttribute()) && $this->getInvoicedToAttribute())
$this->price = 0;
return is_null($this->price)
? $this->product->getBaseChargeAttribute($this->getBillingIntervalAttribute(),$this->account->group)
: $this->price;
}
/** /**
* Get the stage parameters * Get the stage parameters
* *
@ -950,8 +1108,7 @@ class Service extends Model implements IDs
*/ */
public function isActive(): bool public function isActive(): bool
{ {
return $this->attributes['active'] return $this->attributes['active'] || ($this->order_status && (! in_array($this->order_status,self::INACTIVE_STATUS)));
|| ($this->order_status && (! in_array($this->order_status,self::INACTIVE_STATUS)));
} }
/** /**
@ -1023,6 +1180,17 @@ class Service extends Model implements IDs
return ! is_null($this->price); return ! is_null($this->price);
} }
/**
* Should this service be invoiced soon
*
* @param int $days
* @return bool
*/
public function isInvoiceDueSoon($days=30): bool
{
return $this->isBilled() AND $this->getInvoiceNextAttribute()->lessThan(now()->addDays($days));
}
/** /**
* Identify if a service is being ordered, ie: not active yet nor cancelled * Identify if a service is being ordered, ie: not active yet nor cancelled
* *
@ -1038,88 +1206,95 @@ class Service extends Model implements IDs
/** /**
* Generate a collection of invoice_item objects that will be billed for the next invoice * Generate a collection of invoice_item objects that will be billed for the next invoice
* *
* @param bool $future Next item to be billed (not in the next x days)
* @param Carbon|null $billdate * @param Carbon|null $billdate
* @return Collection * @return Collection
* @throws Exception * @throws Exception
* @todo This query is expensive.
*/ */
public function next_invoice_items(Carbon $billdate=NULL): Collection public function next_invoice_items(bool $future,Carbon $billdate=NULL): Collection
{ {
if ($this->wasCancelled() || (! $this->isBilled())) if ($this->wasCancelled() OR (! $this->isBilled()) OR (! $future AND ! $this->active))
return collect(); return collect();
$o = collect(); if (is_null($billdate))
$invoiced_to = $this->getInvoiceNextAttribute(); $billdate = Carbon::now()->addDays(30);
// Connection charges are only charged once, so ignore if if we have already billed them // If pending, add any connection charges
if ((! $this->invoiced_items()->where('item_type',InvoiceItem::INVOICEITEM_SETUP)->count()) // Connection charges are only charged once
&& (InvoiceItem::distinct('invoice_id')->where('service_id',$this->id)->count() < 2) if ((! $this->invoice_items->filter(function($item) { return $item->item_type==4; })->sum('total'))
&& $this->product->getSetupChargeAttribute($this->getBillingIntervalAttribute(),$this->account->group)) AND ($this->isPending() OR is_null($this->invoice_to))
AND $this->product->getSetupChargeAttribute($this->recur_schedule,$this->account->group))
{ {
$ii = new InvoiceItem; $o = new InvoiceItem;
$ii->active = TRUE; $o->active = TRUE;
$ii->service_id = $this->id; $o->service_id = $this->id;
$ii->product_id = $this->product_id; $o->product_id = $this->product_id;
$ii->item_type = InvoiceItem::INVOICEITEM_SETUP; $o->item_type = 4; // @todo change to const or something
$ii->price_base = $this->product->getSetupChargeAttribute($this->getBillingIntervalAttribute(),$this->account->group); $o->price_base = $this->product->getSetupChargeAttribute($this->recur_schedule,$this->account->group);
$ii->start_at = $this->invoice_next; //$o->recurring_schedule = $this->recur_schedule;
$ii->stop_at = $this->invoice_next; $o->start_at = $this->invoice_next;
$ii->quantity = 1; $o->stop_at = $this->invoice_next;
$ii->site_id = 1; // @todo $o->quantity = 1;
$o->site_id = 1; // @todo
$ii->addTaxes($this->account->country->taxes); $o->addTaxes($this->account->country->taxes);
$o->push($ii); $this->invoice_items->push($o);
} }
// The service charges // If the service is active, there will be service charges
if (is_null($billdate)) if ((! $this->invoice_items->filter(function($item) { return $item->item_type==0 AND ! $item->exists; })->count())
$billdate = $invoiced_to->clone()->addDays(config('osb.invoice_days')); AND ($this->active OR $this->isPending())
AND (
(($future == TRUE) AND $this->invoice_next < $this->invoice_next_end) OR
(($future == FALSE) AND ($this->invoice_to < ($this->stop_at ?: $billdate)))
))
{
do {
$o = new InvoiceItem;
$o->active = TRUE;
$o->service_id = $this->id;
$o->product_id = $this->product_id;
$o->item_type = 0;
$o->price_base = is_null($this->price)
? (is_null($this->price) ? $this->product->getBaseChargeAttribute($this->recur_schedule,$this->account->group) : $this->price)
: $this->price; // @todo change to a method in this class
$o->recur_schedule = $this->recur_schedule;
$o->start_at = $this->invoice_next;
$o->stop_at = $this->invoice_next_end;
$o->quantity = $this->invoice_next_quantity;
$o->site_id = 1; // @todo
while ($invoiced_to <= ($this->stop_at ?: $billdate)) { $o->addTaxes($this->account->country->taxes);
$ii = new InvoiceItem; $this->invoice_items->push($o);
$period = Invoice::invoice_period($invoiced_to,$this->getBillingIntervalAttribute(),(bool)$this->product->price_recur_strict); } while ($future == FALSE AND ($this->invoice_to < ($this->stop_at ?: $billdate)));
$ii->active = TRUE;
$ii->service_id = $this->id;
$ii->product_id = $this->product_id;
$ii->item_type = InvoiceItem::INVOICEITEM_SERVICE;
$ii->price_base = $this->billing_charge();
$ii->recur_schedule = $this->getBillingIntervalAttribute();
$ii->start_at = $invoiced_to;
$ii->stop_at = ($this->stop_at && ($this->stop_at < Arr::get($period,'end'))) ? $this->stop_at : Arr::get($period,'end');
$ii->quantity = Invoice::invoice_quantity($ii->start_at,$ii->stop_at,$period);
$ii->site_id = 1; // @todo
$ii->addTaxes($this->account->country->taxes);
$o->push($ii);
$invoiced_to = $ii->stop_at
->clone()
->addDay()
->startOfDay();
} }
// Add additional charges // Add additional charges
foreach ($this->charges->filter(function($item) { return $item->unprocessed; }) as $oo) { if ((($future == TRUE) OR (($future == FALSE) AND ($this->invoice_to >= $billdate)))
$ii = new InvoiceItem; AND ! $this->invoice_items->filter(function($item) { return $item->module_id == 30 AND ! $item->exists; })->count())
{
foreach ($this->charges->filter(function($item) { return $item->unprocessed; }) as $oo) {
$o = new InvoiceItem;
$o->active = TRUE;
$o->service_id = $oo->service_id;
$o->product_id = $this->product_id;
$o->quantity = $oo->quantity;
$o->item_type = $oo->type;
$o->price_base = $oo->amount;
$o->start_at = $oo->start_at;
$o->stop_at = $oo->stop_at;
$o->module_id = 30; // @todo This shouldnt be hard coded
$o->module_ref = $oo->id;
$o->site_id = 1; // @todo
$ii->active = TRUE; $o->addTaxes($this->account->country->taxes);
$ii->service_id = $oo->service_id; $this->invoice_items->push($o);
$ii->product_id = $this->product_id; }
$ii->quantity = $oo->quantity;
$ii->item_type = $oo->type;
$ii->price_base = $oo->amount;
$ii->start_at = $oo->start_at;
$ii->stop_at = $oo->stop_at;
$ii->module_id = 30; // @todo This shouldnt be hard coded
$ii->module_ref = $oo->id;
$ii->site_id = 1; // @todo
$ii->addTaxes($this->account->country->taxes);
$o->push($ii);
} }
return $o; return $this->invoice_items->filter(function($item) { return ! $item->exists; });
} }
/** /**

View File

@ -48,13 +48,11 @@ class Broadband extends Type implements ServiceUsage
* The usage information for broadband * The usage information for broadband
* *
* @return \Illuminate\Database\Eloquent\Relations\HasMany * @return \Illuminate\Database\Eloquent\Relations\HasMany
* @todo rename to usage()
*/ */
public function traffic() public function traffic()
{ {
return $this->hasMany(UsageBroadband::class,'service_item_id') return $this->hasMany(UsageBroadband::class,'service_item_id')
->where('date','>=',Carbon::now()->startOfMonth()); ->where('site_id',$this->site_id);
//->where('site_id',$this->site_id);
} }
/* ATTRIBUTES */ /* ATTRIBUTES */
@ -156,6 +154,8 @@ class Broadband extends Type implements ServiceUsage
if (! $maxdate) if (! $maxdate)
return collect(); return collect();
Log::debug(sprintf('%s:Getting Usage data for [%d] months from [%s]',self::LOGKEY,$months,$maxdate),['m'=>__METHOD__]);
// Go back an extra month; // Go back an extra month;
$start = $maxdate->date->subMonths($months); $start = $maxdate->date->subMonths($months);
@ -166,6 +166,8 @@ class Broadband extends Type implements ServiceUsage
$start = $start->subDays($start->day-15); $start = $start->subDays($start->day-15);
} }
Log::debug(sprintf('%s:Getting Usage data from [%s]',self::LOGKEY,$start->format('Y-m-d')),['m'=>__METHOD__]);
$result = collect(); $result = collect();
foreach ($this->traffic() foreach ($this->traffic()

View File

@ -3,7 +3,6 @@
namespace App\Models\Service; namespace App\Models\Service;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Leenooks\Carbon as LeenooksCarbon; use Leenooks\Carbon as LeenooksCarbon;
use App\Interfaces\ServiceItem; use App\Interfaces\ServiceItem;
@ -42,14 +41,6 @@ abstract class Type extends Model implements ServiceItem
return $this->morphOne(Service::class,'type','model','id','service_id'); return $this->morphOne(Service::class,'type','model','id','service_id');
} }
public function traffic()
{
// Return a null relationship by default, if the child class doesnt track usage (and thus no usage table)
return $this->hasMany(Generic::class,'id')
->where('id',NULL)
->where('site_id',$this->site_id);
}
/* INTERFACE */ /* INTERFACE */
public function getContractTermAttribute(): int public function getContractTermAttribute(): int
@ -106,15 +97,4 @@ abstract class Type extends Model implements ServiceItem
{ {
return $this->service->offering->supplied ?: new \App\Models\Supplier\Generic(); return $this->service->offering->supplied ?: new \App\Models\Supplier\Generic();
} }
/**
* Default usage summary is empty if the underlying model doesnt capture usage
*
* @param int $months
* @return Collection
*/
public function usage_summary(int $months=2): Collection
{
return collect();
}
} }

View File

@ -3,23 +3,25 @@
namespace App\Models\Usage; namespace App\Models\Usage;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use App\Models\Service\Broadband as ServiceBroadband; use App\Models\Service\Broadband as ServiceBroadband;
use Illuminate\Support\Facades\Log;
class Broadband extends Type class Broadband extends Model
{ {
protected $casts = [
'date'=>'datetime:Y-m-d',
];
protected $table = 'usage_broadband'; protected $table = 'usage_broadband';
public $timestamps = FALSE;
private $traffic_end = 14; private $traffic_end = 14;
/* RELATIONS */ /* RELATIONS */
/* @todo rename to service() and put in parent */
/* @deprecated */
public function broadband() public function broadband()
{ {
Log::alert('deprecated function '.__METHOD__);
return $this->belongsTo(ServiceBroadband::class); return $this->belongsTo(ServiceBroadband::class);
} }

View File

@ -1,14 +0,0 @@
<?php
namespace App\Models\Usage;
use Illuminate\Database\Eloquent\Model;
abstract class Type extends Model
{
protected $casts = [
'date'=>'datetime:Y-m-d',
];
public $timestamps = FALSE;
}

View File

@ -5,6 +5,8 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Collection as DatabaseCollection;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Laravel\Passport\HasApiTokens; use Laravel\Passport\HasApiTokens;
use Leenooks\Traits\ScopeActive; use Leenooks\Traits\ScopeActive;
@ -270,6 +272,41 @@ class User extends Authenticatable implements IDs
return in_array($this->role(),['wholesaler']); return in_array($this->role(),['wholesaler']);
} }
/**
* Get all the items for the next invoice
*
* @param bool $future
* @return DatabaseCollection
* @deprecated This should be done in accounts
*/
public function next_invoice_items(bool $future=FALSE): Collection
{
return collect();
$result = new DatabaseCollection;
$this->services->load(['invoice_items.taxes']);
foreach ($this->services as $o) {
if ($future) {
if ($o->invoice_next->subDays(config('app.invoice_inadvance'))->isPast())
continue;
} else {
if ($o->invoice_next->subDays(config('app.invoice_inadvance'))->isFuture())
continue;
}
foreach ($o->next_invoice_items($future) as $oo)
$result->push($oo);
}
$result->load([
'product.translate',
'service.type',
]);
return $result;
}
/** /**
* Determine what the logged in user's role is * Determine what the logged in user's role is
* + Wholesaler - aka Super User * + Wholesaler - aka Super User

View File

@ -14,23 +14,12 @@ trait ScopeServiceActive
*/ */
public function scopeServiceActive($query) public function scopeServiceActive($query)
{ {
return $query return $query->where(function($q) {
->where(fn($q)=> return $q->where('services.active',TRUE)
$q->where('services.active',TRUE) ->orWhere(function($q) {
->orWhere(fn($q)=> return $q->whereNotNull('order_status')
$q->whereNotNull('order_status') ->whereNotIn('services.order_status',Service::INACTIVE_STATUS);
->whereNotIn('services.order_status',Service::INACTIVE_STATUS)) });
); });
}
public function scopeServiceInactive($query)
{
return $query
->where(fn($q)=>
$q->where('services.active',FALSE)
->orWhere(fn($q)=>
$q->whereNotNull('order_status')
->whereIn('services.order_status',Service::INACTIVE_STATUS))
);
} }
} }

View File

@ -3,6 +3,5 @@
return [ return [
'language_id' => 1, 'language_id' => 1,
'invoice_text' => 'Thank you for using our Internet Services.', 'invoice_text' => 'Thank you for using our Internet Services.',
'invoice_days' => 30, // Days in Advance to invoice
'admin' => env('APP_ADMIN'), 'admin' => env('APP_ADMIN'),
]; ];

View File

@ -4,28 +4,55 @@ table.dataTable tr.dtrg-group.dtrg-level-1 td {
} }
/* RENDERING */ /* RENDERING */
/* Spacing between sp and table */
div.dtsp-verticalPanes { div.dtsp-verticalPanes {
margin-right: 1em; margin-right: 10px;
} }
div.dtsp-panesContainer { div.dtsp-panesContainer {
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
width: 18em; width: 15em;
}
div.dtsp-subRow1 {
width: 100%;
}
div.dtsp-searchCont input.dtsp-search.dtsp-disabledButton {
background: #eaeaea;
font-size: larger;
border-radius: 3px;
}
div.dtsp-searchCont input.dtsp-search.dtsp-disabledButton::placeholder,
div.dtsp-searchCont input.dtsp-search.dtsp-disabledButton:-moz-placeholder,
div.dtsp-searchCont input.dtsp-search.dtsp-disabledButton::-moz-placeholder,
div.dtsp-searchCont input.dtsp-search.dtsp-disabledButton::-webkit-input-placeholder {
color: #000000;
font-weight: bold;
} }
div.dtsp-titleRow { div.dtsp-titleRow {
padding: 0.5em; margin-top: 13px;
padding: 5px;
} }
div.dtsp-titleRow button { div.dtsp-titleRow button {
padding: 0 0 0 3px !important; padding: 0 0 0 5px !important;
margin-bottom: 1px;
font-size: 90%; font-size: 90%;
} }
div.dtsp-verticalContainer { div.dtsp-titleRow div.dtsp-title {
padding: 1px;
margin: 0 !important;
font-weight: bolder;
}
div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody div.dtsp-nameCont span.dtsp-pill {
min-width: 4em;
}
div.dtsp-verticalContainer{
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
@ -35,43 +62,40 @@ div.dtsp-verticalContainer {
} }
div.dtsp-verticalContainer div.dtsp-verticalPanes, div.dtsp-verticalContainer div.dtsp-verticalPanes,
div.dtsp-verticalContainer div.dtsp-dataTable { div.dtsp-verticalContainer div.dtsp-dataTable{
width: 50%; width: 50%;
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
flex-basis: 0; flex-basis: 0;
} }
div.dtsp-verticalContainer div.dtsp-verticalPanes { div.dtsp-verticalContainer div.dtsp-verticalPanes{
background: rgba(33, 39, 45, 0.1); background: rgba(33, 39, 45, 0.1);
border-radius: 6px; border-radius: 6px;
border: 1px solid #aaa; border: 1px solid #ccc;
} }
/* Fix Search input */ div.dtsp-title {
div.dtsp-dataTable .dt-search { margin-right: 0px !important;
text-align: right; margin-top: 13px !important;
padding-bottom: 0.5em; margin-left: 5px !important;
} }
/* Fix Table Result */ input.dtsp-search {
div.dtsp-dataTable .dt-info { min-width: 0px !important;
float: left; padding-left: 0px !important;
padding-top: 0.75em; margin: 0px !important;
} }
/* Fix pagination */ div.dtsp-verticalContainer div.dtsp-verticalPanes div.dtsp-searchPanes{
div.dtsp-dataTable .dt-paging { flex-direction: column;
float: right; flex-basis: 0px;
padding-top: 0.5em;
} }
/* Titles */ div.dtsp-verticalContainer div.dtsp-verticalPanes div.dtsp-searchPanes div.dtsp-searchPane{
div.dtsp-panesContainer div.dtsp-searchPane div.dtsp-topRow input.form-control { flex-basis: 0px;
font-weight: bold;
padding-top: 0.5em;
padding-left: 0;
} }
div.dtsp-verticalContainer div.dtsp-dataTable{ div.dtsp-verticalContainer div.dtsp-dataTable{
flex-grow: 1; flex-grow: 1;
flex-shrink: 0; flex-shrink: 0;

View File

@ -8,7 +8,7 @@ Please order the following...
| Account | {{ $service->account->name }} | | Account | {{ $service->account->name }} |
| Service ID | {{ $service->sid }} | | Service ID | {{ $service->sid }} |
| Product | {{ $service->product->name }} | | Product | {{ $service->product->name }} |
@switch($service->product->category) @switch($service->category)
@case('broadband') @case('broadband')
| Address | {{ $service->type->service_address }} | | Address | {{ $service->type->service_address }} |
@break; @break;

View File

@ -9,7 +9,7 @@
| Account | {{ $service->account->name }} | | Account | {{ $service->account->name }} |
| Service ID | {{ $service->sid }} | | Service ID | {{ $service->sid }} |
| Product | {{ $service->product->name }} | | Product | {{ $service->product->name }} |
@switch($service->product->category) @switch($service->category)
@case('broadband') @case('broadband')
| Address | {{ is_object($service->type) ? $service->type->service_address : 'Not Supplied' }} | | Address | {{ is_object($service->type) ? $service->type->service_address : 'Not Supplied' }} |
@break; @break;

View File

@ -8,7 +8,7 @@ Please cancel the following...
| Account | {{ $service->account->name }} | | Account | {{ $service->account->name }} |
| Service ID | {{ $service->sid }} | | Service ID | {{ $service->sid }} |
| Product | {{ $service->product->name }} | | Product | {{ $service->product->name }} |
@switch($service->product->category) @switch($service->category)
@case('broadband') @case('broadband')
| Address | {{ $service->type->service_address }} | | Address | {{ $service->type->service_address }} |
@break; @break;

View File

@ -8,7 +8,7 @@ Please change the following...
| Account | {{ $service->account->name }} | | Account | {{ $service->account->name }} |
| Service ID | {{ $service->sid }} | | Service ID | {{ $service->sid }} |
| Product | {{ $service->product->name }} | | Product | {{ $service->product->name }} |
@switch($service->product->category) @switch($service->category)
@case('broadband') @case('broadband')
| Address | {{ $service->type->service_address }} | | Address | {{ $service->type->service_address }} |
@break; @break;

View File

@ -20,7 +20,7 @@
@endphp @endphp
<tr> <tr>
<td><a href="{{ url('r/switch/start',$ao->user_id) }}"><i class="fas fa-external-link-alt"></i></a></td> <td><a href="{{ url('r/switch/start',$ao->user_id) }}"><i class="fas fa-external-link-alt"></i></a></td>
<td><a href="{{ url('u/home',$ao->user_id) }}">{{ $ao->name }}</a></td> <td>{{ $ao->name }}</td>
<td class="text-right">{{ $ao->services->where('active',TRUE)->count() }} <small>/{{ $ao->services->count() }}</small></td> <td class="text-right">{{ $ao->services->where('active',TRUE)->count() }} <small>/{{ $ao->services->count() }}</small></td>
</tr> </tr>
@endforeach @endforeach

View File

@ -1,35 +0,0 @@
<!-- $o=Account::class -->
@use(Carbon\Carbon)
<!-- Show next items for an invoice -->
@if(($x=$o->invoice_next())->count())
<div class="card">
<div class="card-body">
<p>The following items will be invoiced on or after <strong>{{ max($x->first()->start_at->subDays(config('osb.invoice_days')),Carbon::now())->format('Y-m-d') }}</strong></p>
<table class="table table-sm table-stripped">
<!-- Group by Service -->
@foreach ($x->groupBy('service_id') as $id => $oo)
<tr>
<td>{{ $oo->first()?->product?->category_name ?: '-' }}</td>
<td>
@if($id)
<a href="{{ url('u/service',$oo->first()->service_id) }}">{{ $oo->first()->service->name }}</a>
@else
Account Charges
@endif
</td>
<td class="text-right">${{ number_format($oo->sum('total'),2) }}</td>
</tr>
@endforeach
<tr>
<th class="text-right" colspan="2">TOTAL</th>
<th class="text-right">${{ number_format($x->sum('total'),2) }}</th>
</tr>
</table>
</div>
</div>
@else
<p>No items currently due to invoice.</p>
@endif

View File

@ -27,7 +27,7 @@
<td>{{ $so->product->category_name }}</td> <td>{{ $so->product->category_name }}</td>
<td>{{ $so->name_short }}</td> <td>{{ $so->name_short }}</td>
<td>{{ $so->product->name }}</td> <td>{{ $so->product->name }}</td>
<td>{{ ($so->external_billing || (! $so->invoice_next)) ? '-' : $so->invoice_next->format('Y-m-d') }}</td> <td>{{ $so->external_billing ? '-' : $so->invoice_next->format('Y-m-d') }}</td>
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@ -37,7 +37,7 @@
<div class="info-box-content"> <div class="info-box-content">
<span class="info-box-text">Active Services</span> <span class="info-box-text">Active Services</span>
<span class="info-box-number">{{ Service::ServiceActive()->whereIn('account_id',$acts)->count() }} <small>/{{ Service::whereIn('account_id',$acts)->count() }}</small></span> <span class="info-box-number">{{ Service::active()->whereIn('account_id',$acts)->count() }} <small>/{{ Service::whereIn('account_id',$acts)->count() }}</small></span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -30,7 +30,6 @@
<th>Service</th> <th>Service</th>
<th>Description</th> <th>Description</th>
<th class="text-right">Total</th> <th class="text-right">Total</th>
<th>&nbsp;</th>
</tr> </tr>
</thead> </thead>
@ -43,8 +42,7 @@
<td>{{ $o->account->name }}</td> <td>{{ $o->account->name }}</td>
<td><a href="{{ url('u/service',$o->service_id) }}">{{ $o->service->name_short }}</a></td> <td><a href="{{ url('u/service',$o->service_id) }}">{{ $o->service->name_short }}</a></td>
<td>{{ $o->description }}</td> <td>{{ $o->description }}</td>
<td class="text-right">{{ number_format($o->total,2) }}</td> <td class="text-right">{{ number_format($o->quantity*$o->amount,2) }}</td>
<td><a class="charge_delete text-dark" data-id="{{ $o->id }}" href="{{ url('/r/charge/delete',$o->id) }}"><i class="fas fa-fw fa-ban"></i></a></td>
</tr> </tr>
@empty @empty
<tr> <tr>
@ -57,21 +55,18 @@
</div> </div>
</div> </div>
</div> </div>
<x-leenooks::modal.delete hide="row" trigger="charge_delete"/>
@endsection @endsection
@pa(datatables,rowgroup|conditionalpaging) @section('page-styles')
@css(datatables,bootstrap4)
@append
@section('page-scripts') @section('page-scripts')
@js(datatables,bootstrap4)
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
$('#unprocessed_charges').DataTable({ $('#unprocessed_charges').DataTable( {
conditionalPaging: true, order: [1,'desc'],
order: [[3,'desc'],[1,'desc']],
rowGroup: {
dataSrc: [3],
},
}); });
}); });
</script> </script>

View File

@ -76,7 +76,7 @@
<div class="tab-pane fade" id="tab-futureinvoice"> <div class="tab-pane fade" id="tab-futureinvoice">
<div class="row"> <div class="row">
<div class="col-12 col-xl-9"> <div class="col-12 col-xl-9">
@include('theme.backend.adminlte.account.widget.invoice_next',['o'=>$ao]) @include('theme.backend.adminlte.invoice.widget.next',['future'=>TRUE])
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,41 @@
<!-- @todo These needs to be optimised, and change for $o = Account::class -->
<!-- Show next items for an invoice -->
@if(($x=$o->next_invoice_items($future))->count())
<div class="card">
<div class="card-body">
<table class="table">
<!-- Group by Account -->
@foreach ($x->groupBy('product_id') as $id => $oo)
<tr>
<th colspan="4">{{ $oo->first()->product->name }}</th>
<th class="text-right">${{ number_format($oo->sum('total'),2) }}</th>
</tr>
@foreach ($oo->groupBy('service_id') as $ooo)
<tr>
<td class="pt-0 pb-1" style="width: 12em;"><a href="{{ url('u/service',$ooo->first()->service_id) }}">{{ $ooo->first()->service->sid }}</a></td>
<td class="pt-0 pb-1" colspan="3">{{ $ooo->first()->service->name }}</td>
</tr>
@foreach ($ooo as $io)
<tr>
<td class="pt-0 pb-1">&nbsp;</td>
<td class="pt-0 pb-1">&nbsp;</td>
<td class="pt-0 pb-1">{{ $io->item_type_name }}</td>
<td class="text-right pt-0 pb-1">${{ number_format($io->total,2) }}</td>
</tr>
@endforeach
@endforeach
@endforeach
<tr>
<th class="text-right" colspan="4">TOTAL</th>
<th class="text-right">${{ number_format($x->sum('total'),2) }}</th>
</tr>
</table>
</div>
</div>
@else
<p>No items currently due to invoice.</p>
@endif

View File

@ -32,7 +32,7 @@
@foreach (\App\Models\Service::active()->with(['product.translate'])->get()->groupBy('product_id') as $s) @foreach (\App\Models\Service::active()->with(['product.translate'])->get()->groupBy('product_id') as $s)
<tr> <tr>
<td><a href="{{ url('a/product/details',[($x=$s->first())->product_id]) }}">{{ $x->id }}</a></td> <td><a href="{{ url('a/product/details',[($x=$s->first())->product_id]) }}">{{ $x->id }}</a></td>
<td>{{ $x->product->category_name }}</td> <td>{{ $x->category_name }}</td>
<td>{{ $x->product->pid }}</td> <td>{{ $x->product->pid }}</td>
<td>{{ $x->product->name }}</td> <td>{{ $x->product->name }}</td>
<td class="text-right">{{ $s->count() }}</td> <td class="text-right">{{ $s->count() }}</td>

View File

@ -1,5 +1,3 @@
@use(App\Models\Service)
@extends('adminlte::layouts.app') @extends('adminlte::layouts.app')
@section('htmlheader_title') @section('htmlheader_title')
@ -27,20 +25,21 @@
<th>Product</th> <th>Product</th>
<th class="text-right">Monthly</th> <th class="text-right">Monthly</th>
<th class="text-right">Cost</th> <th class="text-right">Cost</th>
<th class="text-right">Usage</th> <th class="text-right">Traffic (GB)</th>
<th>Supplier</th> <th>Supplier</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (Service::ServiceActive()->with(['account.taxes','type','product.type.supplied.supplier_detail.supplier','product.translate','type.traffic'])->get() as $o) {{-- @todo This query is expensive still --}}
@foreach (\App\Models\Service::active()->with(['type','product.type.supplied.supplier_detail.supplier','product.translate'])->get() as $o)
<tr> <tr>
<td><a href="{{ url('u/service',[$o->id]) }}">{{ $o->id }}</a></td> <td><a href="{{ url('u/service',[$o->id]) }}">{{ $o->id }}</a></td>
<td>{{ $o->name }}</td> <td>{{ $o->name }}</td>
<td>{{ $o->product->name }}</td> <td>{{ $o->product->name }}</td>
<td class="text-right">{{ number_format($o->billing_charge_normalised,2) }}</td> <td class="text-right">{{ number_format($o->billing_monthly_price,2) }}</td>
<td class="text-right">{{ number_format($o->product->cost_normalized(),2) }}</td> <td class="text-right">{{ number_format($o->product->cost_normalized(),2) }}</td>
<td class="text-right">{{ $o->product->hasUsage() ? number_format($o->type->usage_summary(0)->sum()/1000,1) : '-' }}</td> <td class="text-right">{{ $o->category == 'broadband' ? number_format($o->type->usage_summary(0)->sum()/1000,1) : '-' }}</td>
<td>{{ $o->product->supplier->name }}</td> <td>{{ $o->product->supplier->name }}</td>
</tr> </tr>
@endforeach @endforeach
@ -51,60 +50,51 @@
</div> </div>
@endsection @endsection
@pa(datatables,rowgroup|conditionalpaging|select|searchpanes|searchpanes-left)
@section('page-scripts') @section('page-scripts')
@css(datatables,bootstrap4|fixedheader|responsive|rowgroup|buttons)
@js(datatables,bootstrap4|fixedheader|responsive|rowgroup|buttons)
<style>
tr.odd td:first-child,
tr.even td:first-child {
padding-left: 3em;
}
table.dataTable tr.dtrg-group.dtrg-level-1 td {
background-color: #e0e0e0;
color: #4c110f;
}
</style>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
$('#table').DataTable({ $('#table').DataTable({
//oSearch: { sSearch: searchString ? decodeURIComponent(searchString) : '' }, //oSearch: { sSearch: searchString ? decodeURIComponent(searchString) : '' },
aLengthMenu: [ aLengthMenu: [
[25, 50, 100, 200, -1], [25, 50, 100, 200, -1],
[25, 50, 100, 200, "All"] [25, 50, 100, 200, "All"]
], ],
paging: true, paging: true,
pageLength: 25, pageLength: 25,
conditionalPaging: true, lengthChange: true,
lengthChange: true, searching: true,
searching: true, ordering: true,
ordering: true, info: true,
info: true, autoWidth: false,
autoWidth: false, fixedHeader: true,
fixedHeader: true, order: [
order: [ [2,'asc'],
[2,'asc'], [1,'asc'],
[1,'asc'], ],
], rowGroup: {
rowGroup: { dataSrc: [2],
dataSrc: [2], },
}, columnDefs: [
columnDefs: [ {
{ targets: [2],
targets: [2], visible: false,
visible: false, },
}, ],
{ });
targets: [0,1,3,4,5], });
searchPanes: { </script>
show: false,
}
},
],
language: {
searchPanes: {
title: 'Filters: %d',
collapse: 'Filter',
}
},
searchPanes: {
cascadePanes: true,
viewTotal: true,
layout: 'columns-1',
dataLength: 20,
controls: false,
},
dom: '<"dtsp-verticalContainer"<"dtsp-verticalPanes"P><"dtsp-dataTable"Bfrtip>>',
});
});
</script>
@append @append

View File

@ -8,11 +8,11 @@
</div> </div>
@endif @endif
<div class="card-header bg-light p-2"> <div class="card-header bg-light">
<h3 class="card-title">Service Information</h3> <h3 class="card-title">Service Information</h3>
</div> </div>
<div class="card-body p-2"> <div class="card-body bg-light">
<table class="table table-sm"> <table class="table table-sm">
<tr> <tr>
<th>Account</th> <th>Account</th>
@ -28,7 +28,7 @@
<td>{{ $o->order_info_reference ?? '' }}</td> <td>{{ $o->order_info_reference ?? '' }}</td>
</tr> </tr>
@endif @endif
@if($o->start_at && $o->isPending()) @if($o->start_at AND $o->isPending())
<tr> <tr>
<th>Pending Connection</th> <th>Pending Connection</th>
<td>{{ $o->start_at->format('Y-m-d') }}</td> <td>{{ $o->start_at->format('Y-m-d') }}</td>
@ -47,10 +47,10 @@
<td>${{ number_format($o->billing_charge,2) }}</td> <td>${{ number_format($o->billing_charge,2) }}</td>
@endif @endif
</tr> </tr>
@if($o->isActive() && $o->invoiced_to) @if($o->active && $o->invoice_to)
<tr> <tr>
<th>Invoiced To</th> <th>Invoiced To</th>
<td>{{ $o->invoiced_to->format('Y-m-d') }}</td> <td>{{ $o->invoice_to->format('Y-m-d') }}</td>
</tr> </tr>
@if($o->paid_to) @if($o->paid_to)
<tr> <tr>
@ -59,16 +59,14 @@
</tr> </tr>
@endif @endif
@endif @endif
@if($o->status !== 'cancel-pending')
<tr> <tr>
<th>Next Invoice</th> <th>Next Invoice</th>
<td>@if($o->suspend_billing)<del>@endif{{ $o->invoice_next->format('Y-m-d') }}@if($o->suspend_billing)</del> <strong>SUSPENDED</strong>@endif</td> <td>@if($o->suspend_billing)<del>@endif{{ $o->invoice_next->format('Y-m-d') }}@if($o->suspend_billing)</del> <strong>SUSPENDED</strong>@endif</td>
</tr> </tr>
<tr> <tr>
<th>Next Estimated Invoice</th> <th>Next Estimated Invoice</th>
<td>${{ number_format($o->next_invoice_items()->sum('total'),2) }} <sup>*</sup></td> <td>${{ number_format($o->next_invoice_items(TRUE)->sum('total'),2) }} <sup>*</sup></td>
</tr> </tr>
@endif
<tr> <tr>
<th>Payment Method</th> <th>Payment Method</th>
<td>@if($o->billing)Direct Debit @else Invoice @endif</td> <td>@if($o->billing)Direct Debit @else Invoice @endif</td>

View File

@ -1,7 +1,5 @@
@use(App\Models\Invoice)
@php($c=$o->product)
<!-- $o=Service::class, $p=Product::class --> <!-- $o=Service::class, $p=Product::class -->
@php($c=$o->product)
<table class="table table-sm"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
@ -77,16 +75,16 @@
<th>Monthly Price</th> <th>Monthly Price</th>
<td @if($x=$o->isChargeOverridden()) class="text-danger" @endif> <td @if($x=$o->isChargeOverridden()) class="text-danger" @endif>
@if($x) @if($x)
<abbr title="${{ number_format($b=$o->account->taxed($c->base_charge)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}">${{ number_format($b=$o->billing_charge_normalised,2) }} <abbr title="${{ number_format($b=$o->account->taxed($c->base_charge)*\App\Models\Invoice::billing_change($o->billing_interval,\App\Models\Invoice::BILL_MONTHLY),2) }}">${{ number_format($b=$o->billing_monthly_price,2) }}
@else @else
{{ number_format($b=$o->billing_charge_normalised,2) }} {{ number_format($b=$o->account->taxed($c->base_charge)*\App\Models\Invoice::billing_change($o->billing_interval,\App\Models\Invoice::BILL_MONTHLY),2) }}
@endif @endif
</td> </td>
<td>${{ number_format($a=$o->account->taxed($c->base_cost)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}</td> <td>${{ number_format($a=$o->account->taxed($c->base_cost)*\App\Models\Invoice::billing_change($o->billing_interval,\App\Models\Invoice::BILL_MONTHLY),2) }}</td>
<td>{!! markup($a,$b) !!}</td> <td>{!! markup($a,$b) !!}</td>
@if($p->exists) @if($p->exists)
<td @if($x=$o->isChargeOverridden()) class="text-danger" @endif>${{ number_format($b=$o->account->taxed($p->base_charge)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}</td> <td @if($x=$o->isChargeOverridden()) class="text-danger" @endif>${{ number_format($b=$o->account->taxed($p->base_charge)*\App\Models\Invoice::billing_change($o->billing_interval,\App\Models\Invoice::BILL_MONTHLY),2) }}</td>
<td>${{ number_format($a=$o->account->taxed($p->base_cost)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}</td> <td>${{ number_format($a=$o->account->taxed($p->base_cost)*\App\Models\Invoice::billing_change($o->billing_interval,\App\Models\Invoice::BILL_MONTHLY),2) }}</td>
<td>{!! markup($a,$b) !!}</td> <td>{!! markup($a,$b) !!}</td>
@endif @endif
</tr> </tr>

View File

@ -1,6 +1,6 @@
<table class="table"> <table class="table">
<tr> <tr>
<th colspan="3">{{ $o->name }}</th><th class="text-right">${{ number_format(($x=$o->next_invoice_items())->sum('total'),2) }}</th> <th colspan="3">{{ $o->name }}</th><th class="text-right">${{ number_format(($x=$o->next_invoice_items(TRUE))->sum('total'),2) }}</th>
</tr> </tr>
@foreach ($x as $io) @foreach ($x as $io)

View File

@ -1,21 +1,49 @@
<!-- $o=Service\Phone::class --> <!-- $o=Service\Phone::class -->
<div class="row"> <div class="row">
<div class="col-12 col-sm-9 col-md-6 col-xl-5"> <div class="col-12 col-sm-9 col-md-6 col-xl-5">
<x-leenooks::form.text id="service_number" name="phone[service_number]" icon="fa-phone" label="Service Number" old="phone.service_number" :value="$o->service_number"/> @include('adminlte::widget.form_text',[
'label'=>'Service Number',
'icon'=>'fas fa-phone',
'id'=>'service_number',
'old'=>'phone.service_number',
'name'=>'phone[service_number]',
'value'=>$o->service_number ?? '',
])
</div> </div>
<div class="col-12 col-sm-9 col-md-6 col-xl-7"> <div class="col-12 col-sm-9 col-md-6 col-xl-7">
<x-leenooks::form.text id="service_address" name="phone[service_address]" icon="fa-map" label="Service Address" old="phone.service_address" :value="$o->service_address"/> @include('adminlte::widget.form_text',[
'label'=>'Service Address',
'icon'=>'fas fa-map',
'id'=>'service_address',
'old'=>'phone.service_address',
'name'=>'phone[service_address]',
'value'=>$o->service_address ?? '',
])
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-9 col-md-12 col-xl-6"> <div class="col-12 col-sm-9 col-md-12 col-xl-6">
<x-leenooks::form.text id="service_username" name="phone[service_username]" icon="fa-user" label="Service Username" old="phone.service_username" :value="$o->service_username"/> @include('adminlte::widget.form_text',[
'label'=>'Service Username',
'icon'=>'fas fa-user',
'id'=>'service_username',
'old'=>'phone.service_username',
'name'=>'phone[service_username]',
'value'=>$o->service_username ?? '',
])
</div> </div>
<div class="col-12 col-sm-9 col-md-5 col-xl-6"> <div class="col-12 col-sm-9 col-md-5 col-xl-6">
<x-leenooks::form.text id="service_password" name="phone[service_password]" icon="fa-lock" label="Service Password" old="phone.service_password" :value="$o->service_password"/> @include('adminlte::widget.form_text',[
'label'=>'Service Password',
'icon'=>'fas fa-lock',
'id'=>'service_password',
'old'=>'phone.service_password',
'name'=>'phone[service_password]',
'value'=>$o->service_password ?? '',
])
</div> </div>
</div> </div>
@ -24,10 +52,24 @@
<div class="row"> <div class="row">
<div class="col-12 col-sm-9 col-md-6 col-xl-5"> <div class="col-12 col-sm-9 col-md-6 col-xl-5">
<x-leenooks::form.date id="connect_at" name="phone[connect_at]" icon="fa-calendar" label="Connect Date" old="phone.connect_at" :value="$o->connect_at?->format('Y-m-d')"/> @include('adminlte::widget.form_date',[
'label'=>'Connect Date',
'icon'=>'fas fa-calendar',
'id'=>'connect_at',
'old'=>'phone.connect_at',
'name'=>'phone[connect_at]',
'value'=>$o->connect_at ? $o->connect_at->format('Y-m-d') : '',
])
</div> </div>
<div class="col-12 col-sm-9 col-md-6 col-xl-5"> <div class="col-12 col-sm-9 col-md-6 col-xl-5">
<x-leenooks::form.date id="expire_at" name="phone[expire_at]" icon="fa-calendar" label="Contract End" old="phone.expire_at" :value="$o->expire_at?->format('Y-m-d') ?: ($o->connect_at?->addMonths($o->contract_term)->format('Y-m-d'))"/> @include('adminlte::widget.form_date',[
'label'=>'Contract End',
'icon'=>'fas fa-calendar',
'id'=>'expire_at',
'old'=>'phone.expire_at',
'name'=>'phone[expire_at]',
'value'=>$o->expire_at ? $o->expire_at->format('Y-m-d') : ($o->connect_at ? $o->connect_at->addMonths($o->contract_term)->format('Y-m-d') : ''),
])
</div> </div>
</div> </div>

View File

@ -121,7 +121,7 @@ Route::group(['middleware'=>['auth','role:wholesaler'],'prefix'=>'a'],function()
Route::get('report/accounts',[ReportController::class,'accounts']); Route::get('report/accounts',[ReportController::class,'accounts']);
Route::get('report/products',[ReportController::class,'products']); Route::get('report/products',[ReportController::class,'products']);
Route::view('report/services','theme.backend.adminlte.service.report'); Route::get('report/services',[ReportController::class,'services']);
// Payments - @todo This should probably go to resellers // Payments - @todo This should probably go to resellers
Route::match(['get','post'],'payment/addedit/{o?}',[AdminController::class,'pay_addedit']); Route::match(['get','post'],'payment/addedit/{o?}',[AdminController::class,'pay_addedit']);
@ -172,6 +172,9 @@ Route::group(['middleware'=>['auth'],'prefix'=>'u'],function() {
Route::get('home/{o}',[HomeController::class,'home']) Route::get('home/{o}',[HomeController::class,'home'])
->where('o','[0-9]+') ->where('o','[0-9]+')
->middleware('can:view,o'); ->middleware('can:view,o');
// Route::get('account/{o}/invoice','User\AccountController@view_invoice_next')
// ->where('o','[0-9]+')
// ->middleware('can:view,o');
Route::post('checkout/pay',[CheckoutController::class,'pay']); Route::post('checkout/pay',[CheckoutController::class,'pay']);
Route::get('invoice/{o}',[InvoiceController::class,'view']) Route::get('invoice/{o}',[InvoiceController::class,'view'])
->where('o','[0-9]+') ->where('o','[0-9]+')