Add account next invoice

This commit is contained in:
Deon George 2024-07-31 22:36:28 +10:00
parent 0b5bc9e012
commit f43748e20a
9 changed files with 109 additions and 96 deletions

View File

@ -37,7 +37,8 @@ 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->delete(); $o->active = FALSE;
$o->save();
return ['ok']; return ['ok'];

View File

@ -2,6 +2,7 @@
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;
@ -252,11 +253,59 @@ 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
* @return Collection * @param bool $all
* @return Builder
*/ */
public function invoiceSummary(Collection $invoices=NULL,bool $all=FALSE): Builder public function invoiceSummary(Collection $invoices=NULL,bool $all=FALSE): Builder
{ {

View File

@ -2,6 +2,7 @@
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;
@ -11,7 +12,6 @@ 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;
@ -33,8 +33,6 @@ use App\Traits\{ScopeServiceActive,ScopeServiceUserAuthorised};
* + 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 * + invoiced_to : When this service has been billed 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
@ -615,10 +613,13 @@ class Service extends Model implements IDs
* *
* @return Carbon * @return Carbon
*/ */
public function getInvoiceNextAttribute(): Carbon public function getInvoiceNextAttribute(): ?Carbon
{ {
$last = $this->getInvoicedToAttribute(); $last = $this->getInvoicedToAttribute();
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());
@ -1074,9 +1075,9 @@ class Service extends Model implements IDs
if (is_null($billdate)) if (is_null($billdate))
$billdate = $invoiced_to->clone()->addDays(config('osb.invoice_days')); $billdate = $invoiced_to->clone()->addDays(config('osb.invoice_days'));
while ($invoiced_to < ($this->stop_at ?: $billdate)) { while ($invoiced_to <= ($this->stop_at ?: $billdate)) {
$ii = new InvoiceItem; $ii = new InvoiceItem;
$period = Invoice::invoice_period($invoiced_to,$this->getBillingIntervalAttribute(),$this->product->price_recur_strict); $period = Invoice::invoice_period($invoiced_to,$this->getBillingIntervalAttribute(),(bool)$this->product->price_recur_strict);
$ii->active = TRUE; $ii->active = TRUE;
$ii->service_id = $this->id; $ii->service_id = $this->id;

View File

@ -5,8 +5,6 @@ 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;
@ -272,41 +270,6 @@ 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

@ -0,0 +1,35 @@
<!-- $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->format('Y-m-d') }}</td> <td>{{ ($so->external_billing || (! $so->invoice_next)) ? '-' : $so->invoice_next->format('Y-m-d') }}</td>
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>

View File

@ -30,6 +30,7 @@
<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>
@ -42,7 +43,8 @@
<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->quantity*$o->amount,2) }}</td> <td class="text-right">{{ number_format($o->total,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>
@ -55,18 +57,21 @@
</div> </div>
</div> </div>
</div> </div>
<x-leenooks::modal.delete hide="row" trigger="charge_delete"/>
@endsection @endsection
@section('page-styles') @pa(datatables,rowgroup|conditionalpaging)
@css(datatables,bootstrap4)
@append
@section('page-scripts')
@js(datatables,bootstrap4)
@section('page-scripts')
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
$('#unprocessed_charges').DataTable({ $('#unprocessed_charges').DataTable({
order: [1,'desc'], conditionalPaging: true,
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.invoice.widget.next',['future'=>TRUE]) @include('theme.backend.adminlte.account.widget.invoice_next',['o'=>$ao])
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,41 +0,0 @@
<!-- @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