diff --git a/app/Console/Commands/ImportCosts.php b/app/Console/Commands/ImportCosts.php new file mode 100644 index 0000000..43f40bd --- /dev/null +++ b/app/Console/Commands/ImportCosts.php @@ -0,0 +1,41 @@ +argument('siteid')), + Supplier::where('name',$this->argument('supplier'))->singleOrFail(), + Carbon::create($this->argument('date')), + $this->argument('file'), + ); + } +} diff --git a/app/Http/Controllers/CostController.php b/app/Http/Controllers/CostController.php new file mode 100644 index 0000000..af3de83 --- /dev/null +++ b/app/Http/Controllers/CostController.php @@ -0,0 +1,15 @@ +$o]); + } +} diff --git a/app/Jobs/ImportCosts.php b/app/Jobs/ImportCosts.php new file mode 100644 index 0000000..8060f5d --- /dev/null +++ b/app/Jobs/ImportCosts.php @@ -0,0 +1,228 @@ +file = $file; + Config::set('site',$site); + + $this->co = Cost::where('site_id',$site->id) + ->where('supplier_id',$so->id) + ->where('billed_at',$invoice_date) + ->firstOrNew(); + $this->co->active = TRUE; + $this->co->site_id = $site->id; + $this->co->billed_at = $invoice_date; + $this->co->supplier_id = $so->id; + $this->co->save(); + + Cost\Broadband::where('cost_id',$this->co->id)->where('site_id',$site->id)->delete(); + Cost\Phone::where('cost_id',$this->co->id)->where('site_id',$site->id)->delete(); + Cost\Generic::where('cost_id',$this->co->id)->where('site_id',$site->id)->delete(); + + // @todo to be stored in supplier config + $headers = [ + 'INVOICEID'=>'Item ID', + 'REF'=>'Reference No', + 'IDTAG'=>'ID Tag', + 'CATEGORY'=>'Category', + 'DESC'=>'Item Description', + 'QUANTITY'=>'Quantity', + 'PRICEUNIT'=>'Unit Price (inc-GST)', + 'PRICETOTAL'=>'Total (inc-GST)' + ]; + + $this->columns = collect($headers)->transform(function($item) { return strtolower($item); }); + } + + /** + * Execute the job. + * + * @return void + * @throws \Exception + */ + public function handle() + { + $skip = 7; // @todo to be stored in supplier config + + $file = fopen($this->file,'r'); + $haveHeader = FALSE; + + $c = 0; + while (! feof($file)) { + $line = stream_get_line($file,0,"\r\n"); + if (str_starts_with($line,'#')) + continue; + + // Remove any embedded CR and BOM + $line = str_replace("\r",'',$line); + $line = preg_replace('/^\x{feff}/u','',$line); + if (($c++ < $skip) || (! $line)) + continue; + + // The first line is a header. + if (! $haveHeader) { + Log::debug(sprintf('%s: Input File: %s',get_class($this),$this->file)); + Log::debug(sprintf('%s: Processing columns: %s',get_class($this),join('|',$this->setColumns($line)->toArray()))); + $haveHeader = TRUE; + continue; + } + + // If the line has a , between two (), then convert the comma to a space. + $x = []; + if (preg_match('#\(.+,.+\)#i',$line,$x)) { + $replace = str_replace(',','_',$x[0]); + $line = str_replace($x[0],$replace,$line); + //dd($line,$x); + } + + $fields = str_getcsv(trim($line)); + if (is_null($x=$this->getColumnKey('DESC')) OR empty($fields[$x])) + continue; + + // The first part of our item description is the service number. + // This should go to a "supplier" function, since all suppliers may show different values in description. + $m = []; + $desc = $fields[$x]; + // m[1] = Service, m[2] = Desc, m[3] = From Date, m[4] = To Date + preg_match('#^([0-9]{10})\s+-\s+(.*)\(([0-9]+\s+[JFMASOND].*\s+[0-9]+)+\s+-\s+([0-9]+\s+[JFMASOND].*\s+[0-9]+)+\)$#',$fields[$x],$m); + + if (count($m) !== 5) + throw new \Exception(sprintf('ERROR: Description didnt parse [%s] on line [%d]',$fields[$x],$c)); + + $cost = ($x=$this->getColumnKey('PRICETOTAL')) ? str_replace([',','$'],'',$fields[$x]) : NULL; + $start_at = Carbon::createFromFormat('d M Y',$m[3]); + $stop_at = Carbon::createFromFormat('d M Y',$m[4]); + $so = Service::search($m[1])->active()->with(['type','product.type.supplied'])->single(); + + if ($so) { + // r[1] = Monthly Charge or Extra Charge,r[2] = "On Plan", r[3] = Plan Info + $r = []; + switch ($so->category) { + case 'broadband': + $to = Cost\Broadband::where('site_id',$this->co->site_id) + ->where('cost_id',$this->co->id) + ->where('service_broadband_id',$so->type->id) + ->where('start_at',$start_at) + ->where('end_at',$stop_at) + ->firstOrNew(); + $to->service_broadband_id = $so->type->id; + + preg_match('#^(Monthly Internet Charge|Plan Change Fee|Change billing date refund for Monthly Internet Charge On Plan|First 12 Month VISP broadband plan discount|.*)\s?(On Plan)?\s?(.*)#',$m[2],$r); + + switch ($r[1]) { + case 'Monthly Internet Charge': + case 'First 12 Month VISP broadband plan discount': + case 'Change billing date refund for Monthly Internet Charge On Plan': + $to->base =+ $cost; + break; + + case 'Plan Change Fee': + $to->excess =+ $cost; + break; + + default: + dump(['extra charge'=>$r]); + $to->excess =+ $cost; + } + + break; + + case 'phone': + $to = Cost\Phone::where('site_id',$this->co->site_id) + ->where('cost_id',$this->co->id) + ->where('service_phone_id',$so->type->id) + ->where('start_at',$start_at) + ->where('end_at',$stop_at) + ->firstOrNew(); + $to->service_phone_id = $so->type->id; + + preg_match('#^(Residential VOIP Plan Excess Usage|Virtual FAX Number Monthly Rental|Corporate VOIP Plan Monthly Rental|Residential VOIP Plan Monthly Rental|.*)\s?(.*)#',$m[2],$r); + + switch ($r[1]) { + case 'Residential VOIP Plan Monthly Rental': + case 'Virtual FAX Number Monthly Rental': + case 'Corporate VOIP Plan Monthly Rental': + $to->base =+ $cost; + break; + + case 'Residential VOIP Plan Excess Usage': + $to->excess =+ $cost; + $to->notes = $r[2]; + break; + + default: + dump(['extra charge'=>$r]); + $to->excess =+ $cost; + } + + break; + + default: + dump(['so'=>$so,'line'=>$line]); + throw new \Exception(sprintf('ERROR: Service type not handled for service [%s] (%s) on line [%d]',$m[1],$so->category,$c)); + } + + } else { + dump(['line'=>$line,'sql'=>Service::search($m[1])->active()->with(['type','product.type.supplied'])->toSql()]); + + $to = Cost\Generic::where('site_id',$this->co->site_id) + ->where('cost_id',$this->co->id) + ->where('notes',sprintf('%s:%s',$m[1],$m[2])) + ->where('start_at',$start_at) + ->where('end_at',$stop_at) + ->firstOrNew(); + + $to->excess =+ $cost; + $to->notes = $line; + } + + $to->site_id = $this->co->site_id; + $to->cost_id = $this->co->id; + $to->active = TRUE; + $to->start_at = $start_at; + $to->end_at = $stop_at; + + // Work out supplier product number + Log::warning(sprintf('%s:Supplier product ID not matched',self::LOGKEY),['r'=>$r]); + + //dd($m[2],$cost,$so->product->type->supplied); + + // Work out if this base charge, or extra charge + + //dd(['M'=>__METHOD__,'fields'=>$fields,'DESC'=>$this->getColumnKey('DESC'),'desc'=>$desc,'m'=>$m,'sql'=>$so->toSql(),'bindings'=>$so->getBindings(),'so'=>$so]); + $to->save(); + } + + fclose($file); + } +} diff --git a/app/Models/Cost.php b/app/Models/Cost.php new file mode 100644 index 0000000..98bd986 --- /dev/null +++ b/app/Models/Cost.php @@ -0,0 +1,40 @@ +hasMany(Broadband::class) + ->where('active',TRUE); + } + + public function generics() + { + return $this->hasMany(Generic::class) + ->where('active',TRUE); + } + + public function phones() + { + return $this->hasMany(Phone::class) + ->where('active',TRUE); + } + + /* ATTRIBUTES */ + +} \ No newline at end of file diff --git a/app/Models/Cost/Broadband.php b/app/Models/Cost/Broadband.php new file mode 100644 index 0000000..9c3f8fc --- /dev/null +++ b/app/Models/Cost/Broadband.php @@ -0,0 +1,18 @@ +hasOneThrough(Service::class,BroadbandService::class,'id','id','service_broadband_id','service_id'); + } +} \ No newline at end of file diff --git a/app/Models/Cost/Generic.php b/app/Models/Cost/Generic.php new file mode 100644 index 0000000..87bcaa6 --- /dev/null +++ b/app/Models/Cost/Generic.php @@ -0,0 +1,18 @@ +hasOneThrough(Service::class,GenericService::class,'id','id','service_generic_id','service_id'); + } +} \ No newline at end of file diff --git a/app/Models/Cost/Phone.php b/app/Models/Cost/Phone.php new file mode 100644 index 0000000..c15a2be --- /dev/null +++ b/app/Models/Cost/Phone.php @@ -0,0 +1,18 @@ +hasOneThrough(Service::class,PhoneService::class,'id','id','service_phone_id','service_id'); + } +} \ No newline at end of file diff --git a/app/Models/Cost/Type.php b/app/Models/Cost/Type.php new file mode 100644 index 0000000..bc82271 --- /dev/null +++ b/app/Models/Cost/Type.php @@ -0,0 +1,26 @@ +base+$this->excess; + } +} \ No newline at end of file diff --git a/app/Models/Policies/CostPolicy.php b/app/Models/Policies/CostPolicy.php new file mode 100644 index 0000000..55a4c71 --- /dev/null +++ b/app/Models/Policies/CostPolicy.php @@ -0,0 +1,17 @@ +isWholesaler(); + } +} \ No newline at end of file diff --git a/app/Traits/Import.php b/app/Traits/Import.php new file mode 100644 index 0000000..c6757e3 --- /dev/null +++ b/app/Traits/Import.php @@ -0,0 +1,43 @@ +columns) + throw new \Exception('ERROR: Columns must be set before calling setColumns()'); + + $this->_columns = collect(explode(',',strtolower($line)))->filter(); + + return $this->_columns->intersect($this->columns); + } + + /** + * Get the index for the column in the file + * + * @param string $key + * @return mixed + */ + private function getColumnKey(string $key) + { + return $this->_columns->search($this->columns->get($key)); + } +} \ No newline at end of file diff --git a/database/migrations/2022_06_13_230224_costs.php b/database/migrations/2022_06_13_230224_costs.php new file mode 100644 index 0000000..57ff235 --- /dev/null +++ b/database/migrations/2022_06_13_230224_costs.php @@ -0,0 +1,101 @@ +down(); + Schema::create('costs', function (Blueprint $table) { + $table->integer('id',TRUE,TRUE); + $table->timestamps(); + $table->integer('site_id')->unsigned(); + $table->date('billed_at'); + $table->boolean('active'); + $table->integer('supplier_id')->unsigned(); + $table->string('invoice_num')->nullable(); + + $table->unique(['id','site_id']); + $table->foreign(['site_id'])->references(['site_id'])->on('sites'); + $table->foreign(['supplier_id'])->references(['id'])->on('suppliers'); + }); + + Schema::create('cost_broadband', function (Blueprint $table) { + $table->integer('id',TRUE,TRUE); + $table->integer('site_id')->unsigned(); + $table->integer('cost_id')->unsigned(); + $table->boolean('active'); + $table->integer('service_broadband_id')->unsigned()->nullable(); + $table->integer('supplier_broadband_id')->unsigned()->nullable(); + $table->date('start_at')->nullable(); + $table->date('end_at')->nullable(); + $table->float('base')->nullable(); + $table->float('excess')->nullable(); + $table->string('reference')->nullable(); + $table->text('notes')->nullable(); + + $table->foreign(['cost_id','site_id'])->references(['id','site_id'])->on('costs'); + $table->foreign(['service_broadband_id','site_id'])->references(['id','site_id'])->on('service_broadband'); + $table->foreign(['supplier_broadband_id','site_id'])->references(['id','site_id'])->on('supplier_broadband'); + }); + + Schema::create('cost_phone', function (Blueprint $table) { + $table->integer('id',TRUE,TRUE); + $table->integer('site_id')->unsigned(); + $table->integer('cost_id')->unsigned(); + $table->boolean('active'); + $table->integer('service_phone_id')->unsigned()->nullable(); + $table->integer('supplier_phone_id')->unsigned()->nullable(); + $table->date('start_at')->nullable(); + $table->date('end_at')->nullable(); + $table->float('base')->nullable(); + $table->float('excess')->nullable(); + $table->string('reference')->nullable(); + $table->text('notes')->nullable(); + + $table->foreign(['cost_id','site_id'])->references(['id','site_id'])->on('costs'); + $table->foreign(['service_phone_id','site_id'])->references(['id','site_id'])->on('service_phone'); + $table->foreign(['supplier_phone_id','site_id'])->references(['id','site_id'])->on('supplier_phone'); + }); + + Schema::create('cost_generic', function (Blueprint $table) { + $table->integer('id',TRUE,TRUE); + $table->integer('site_id')->unsigned(); + $table->integer('cost_id')->unsigned(); + $table->boolean('active'); + $table->integer('service_generic_id')->unsigned()->nullable(); + $table->integer('supplier_generic_id')->unsigned()->nullable(); + $table->date('start_at')->nullable(); + $table->date('end_at')->nullable(); + $table->float('base')->nullable(); + $table->float('excess')->nullable(); + $table->string('reference')->nullable(); + $table->text('notes')->nullable(); + + $table->foreign(['cost_id','site_id'])->references(['id','site_id'])->on('costs'); + $table->foreign(['service_generic_id','site_id'])->references(['id','site_id'])->on('service_generic'); + $table->foreign(['supplier_generic_id','site_id'])->references(['id','site_id'])->on('supplier_generic'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('cost_generic'); + Schema::dropIfExists('cost_phone'); + Schema::dropIfExists('cost_broadband'); + Schema::dropIfExists('costs'); + } +}; diff --git a/resources/views/theme/backend/adminlte/a/cost/home.blade.php b/resources/views/theme/backend/adminlte/a/cost/home.blade.php new file mode 100644 index 0000000..a179f1e --- /dev/null +++ b/resources/views/theme/backend/adminlte/a/cost/home.blade.php @@ -0,0 +1,165 @@ +@extends('adminlte::layouts.app') + +@section('htmlheader_title') + Cost #{{ $o->id }} +@endsection + +@section('contentheader_title') + Cost #{{ $o->id }} +@endsection +@section('contentheader_description') + +@endsection + + +@php($cost = 0) +@php($charge = 0) +@section('main-content') +
Category | +Service | +Active | +From | +To | +Base | +Excess | +Cost | +Charge | +Profit | +
---|---|---|---|---|---|---|---|---|---|
Broadband | ++ | + | {{ $o->broadbands->min('start_at')->format('Y-m-d') }} | +{{ $o->broadbands->max('end_at')->format('Y-m-d') }} | +${{ number_format($a=$o->broadbands->sum('base'),2) }} | +${{ number_format($o->broadbands->sum('excess'),2) }} | +${{ number_format($o->broadbands->sum('cost'),2) }} | +${{ number_format($b=$o->broadbands->sum('service.billing_monthly_price'),2) }} | +${{ number_format($b-$a,2) }} | +
+ | {{ $oo->first()->service->name }} | +{{ $oo->first()->service->active ? 'YES' : 'NO' }} | +{{ $oo->min('start_at')->format('Y-m-d') }} | +{{ $oo->max('end_at')->format('Y-m-d') }} | +${{ number_format($a=$oo->sum('base'),2) }} | +${{ number_format($oo->sum('excess'),2) }} | +${{ number_format($oo->sum('cost'),2) }} | +${{ number_format($b=$oo->first()->service->billing_monthly_price,2) }} | +${{ number_format($b-$a,2) }} | +
Phone | ++ | + | {{ $o->phones->min('start_at')->format('Y-m-d') }} | +{{ $o->phones->max('end_at')->format('Y-m-d') }} | +${{ number_format($a=$o->phones->sum('base'),2) }} | +${{ number_format($o->phones->sum('excess'),2) }} | +${{ number_format($o->phones->sum('cost'),2) }} | +${{ number_format($b=$o->phones->sum('service.billing_monthly_price'),2) }} | +${{ number_format($b-$a,2) }} | +
+ | {{ $oo->first()->service->name }} | +{{ $oo->first()->service->active ? 'YES' : 'NO' }} | +{{ $oo->min('start_at')->format('Y-m-d') }} | +{{ $oo->max('end_at')->format('Y-m-d') }} | +${{ number_format($a=$oo->sum('base'),2) }} | +${{ number_format($oo->sum('excess'),2) }} | +${{ number_format($oo->sum('cost'),2) }} | +${{ number_format($b=$oo->first()->service->billing_monthly_price,2) }} | +${{ number_format($b-$a,2) }} | +
Generic | ++ | + | {{ $o->generics->min('start_at')->format('Y-m-d') }} | +{{ $o->generics->max('end_at')->format('Y-m-d') }} | +${{ number_format($a=$o->generics->sum('base'),2) }} | +${{ number_format($o->generics->sum('excess'),2) }} | +${{ number_format($o->generics->sum('cost'),2) }} | +$0.00 | +${{ number_format(0-$a,2) }} | +
+ | {{ ($x=$oo->first()->service) ? $x->name : '-' }} | +{{ $x ? ($x->active ? 'YES' : 'NO') : '-' }} | +{{ $oo->min('start_at')->format('Y-m-d') }} | +{{ $oo->max('end_at')->format('Y-m-d') }} | +${{ number_format($a=$oo->sum('base'),2) }} | +${{ number_format($oo->sum('excess'),2) }} | +${{ number_format($oo->sum('cost'),2) }} | +${{ number_format($b=$x ? $x->billing_monthly_price : 0,2) }} | +${{ number_format($b-$a,2) }} | +
TOTAL | +${{ number_format($cost,2) }} | +${{ number_format($charge,2) }} | +${{ number_format($charge-$cost,2) }} | +