From ad2f6f3a7f5dfe9405bf3f155b7a0c6cd3e21ce9 Mon Sep 17 00:00:00 2001 From: Deon George Date: Fri, 12 May 2023 20:09:51 +1000 Subject: [PATCH] Inuit sync of tax, product accounting, accounts and invoices --- app/Console/Commands/AccountingAccountAdd.php | 70 +++++------ app/Console/Commands/AccountingAccountGet.php | 58 +++++++++ .../Commands/AccountingAccountSync.php | 49 ++++---- app/Console/Commands/AccountingInvoiceAdd.php | 110 ++++++++++++++++++ app/Console/Commands/AccountingInvoiceGet.php | 58 +++++++++ app/Console/Commands/AccountingTaxSync.php | 51 ++++++++ app/Http/Controllers/AccountingController.php | 12 +- app/Http/Controllers/ProductController.php | 11 +- app/Http/Requests/ProductAddEdit.php | 2 +- app/Jobs/AccountingAccountSync.php | 100 +++++++++------- app/Jobs/AccountingTaxSync.php | 86 ++++++++++++++ app/Models/Invoice.php | 7 ++ app/Models/InvoiceItemTax.php | 7 ++ app/Models/Product.php | 11 +- app/Models/ProviderOauth.php | 33 ++++-- app/Models/ProviderToken.php | 28 +++++ app/Models/Tax.php | 10 ++ app/Traits/ProviderRef.php | 16 +++ config/services.php | 22 ++-- .../2023_05_10_215107_accounting_invoice.php | 70 +++++++++++ .../adminlte/product/widget/detail.blade.php | 38 ++++-- .../service/widget/internal.blade.php | 2 +- 22 files changed, 707 insertions(+), 144 deletions(-) create mode 100644 app/Console/Commands/AccountingAccountGet.php create mode 100644 app/Console/Commands/AccountingInvoiceAdd.php create mode 100644 app/Console/Commands/AccountingInvoiceGet.php create mode 100644 app/Console/Commands/AccountingTaxSync.php create mode 100644 app/Jobs/AccountingTaxSync.php create mode 100644 app/Traits/ProviderRef.php create mode 100644 database/migrations/2023_05_10_215107_accounting_invoice.php diff --git a/app/Console/Commands/AccountingAccountAdd.php b/app/Console/Commands/AccountingAccountAdd.php index c5bf30d..81318e4 100644 --- a/app/Console/Commands/AccountingAccountAdd.php +++ b/app/Console/Commands/AccountingAccountAdd.php @@ -5,58 +5,58 @@ namespace App\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\Config; use Intuit\Jobs\AccountingCustomerUpdate; -use Intuit\Models\Customer; +use Intuit\Models\Customer as AccAccount; use App\Models\{Account,ProviderOauth,Site,User}; class AccountingAccountAdd extends Command { - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'accounting:account:add' + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'accounting:account:add' .' {siteid : Site ID}' .' {provider : Provider Name}' .' {user : User Email}' .' {id : Account ID}'; - /** - * The console command description. - * - * @var string - */ - protected $description = 'Add an account to the accounting provider'; + /** + * The console command description. + * + * @var string + */ + protected $description = 'Add an account to the accounting provider'; - /** - * Execute the console command. - * - * @return int - */ - public function handle() - { + /** + * Execute the console command. + * + * @return int + */ + public function handle() + { $site = Site::findOrFail($this->argument('siteid')); Config::set('site',$site); - $so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail(); $uo = User::where('email',$this->argument('user'))->singleOrFail(); - if (($x=$so->tokens->where('user_id',$uo->id))->count() !== 1) + $so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail(); + if (! ($to=$so->token($uo))) abort(500,sprintf('Unknown Tokens for [%s]',$uo->email)); - $ao = Account::findOrFail($this->argument('id')); + $o = Account::findOrFail($this->argument('id')); - $customer = new Customer; - $customer->PrimaryEmailAddr = (object)['Address'=>$ao->user->email]; - $customer->ResaleNum = $ao->sid; - $customer->GivenName = $ao->user->firstname; - $customer->FamilyName = $ao->user->lastname; - $customer->CompanyName = $ao->name; - $customer->DisplayName = $ao->name; - $customer->FullyQualifiedName = $ao->name; - $customer->Active = (bool)$ao->active; + $acc = new AccAccount; + $acc->PrimaryEmailAddr = (object)['Address'=>$o->user->email]; + $acc->ResaleNum = $o->sid; + $acc->GivenName = $o->user->firstname; + $acc->FamilyName = $o->user->lastname; + $acc->CompanyName = $o->name; + $acc->DisplayName = $o->name; + $acc->FullyQualifiedName = $o->name; + $acc->Active = (bool)$o->active; - return AccountingCustomerUpdate::dispatchSync($x->pop(),$customer); - } -} + return AccountingCustomerUpdate::dispatchSync($to,$acc); + } +} \ No newline at end of file diff --git a/app/Console/Commands/AccountingAccountGet.php b/app/Console/Commands/AccountingAccountGet.php new file mode 100644 index 0000000..8fe76a5 --- /dev/null +++ b/app/Console/Commands/AccountingAccountGet.php @@ -0,0 +1,58 @@ +argument('siteid')); + Config::set('site',$site); + + $uo = User::where('email',$this->argument('user'))->singleOrFail(); + + $so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail(); + if (! ($to=$so->token($uo))) + abort(500,sprintf('Unknown Tokens for [%s]',$uo->email)); + + try { + $api = $to->API(); + dump($api->getAccountQuery($this->argument('id'))); + + } catch (ConnectException|ConnectionIssueException $e) { + $this->error($e->getMessage()); + + return Command::FAILURE; + } + } +} \ No newline at end of file diff --git a/app/Console/Commands/AccountingAccountSync.php b/app/Console/Commands/AccountingAccountSync.php index 8ce7cab..7f988e8 100644 --- a/app/Console/Commands/AccountingAccountSync.php +++ b/app/Console/Commands/AccountingAccountSync.php @@ -8,41 +8,44 @@ use Illuminate\Support\Facades\Config; use App\Models\{ProviderOauth,Site,User}; use App\Jobs\AccountingAccountSync as Job; +/** + * Synchronise Customers with Accounts + */ class AccountingAccountSync extends Command { - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'accounting:account:sync' + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'accounting:account:sync' .' {siteid : Site ID}' .' {provider : Provider Name}' .' {user : User Email}'; - /** - * The console command description. - * - * @var string - */ - protected $description = 'Synchronise accounts with accounting system'; + /** + * The console command description. + * + * @var string + */ + protected $description = 'Synchronise accounts with accounting system'; - /** - * Execute the console command. - * - * @return int - */ - public function handle() - { + /** + * Execute the console command. + * + * @return int + */ + public function handle() + { $site = Site::findOrFail($this->argument('siteid')); Config::set('site',$site); - $so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail(); $uo = User::where('email',$this->argument('user'))->singleOrFail(); - if (($x=$so->tokens->where('user_id',$uo->id))->count() !== 1) + $so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail(); + if (! ($to=$so->token($uo))) abort(500,sprintf('Unknown Tokens for [%s]',$uo->email)); - Job::dispatchSync($x->pop()); - } + Job::dispatchSync($to); + } } \ No newline at end of file diff --git a/app/Console/Commands/AccountingInvoiceAdd.php b/app/Console/Commands/AccountingInvoiceAdd.php new file mode 100644 index 0000000..417fe56 --- /dev/null +++ b/app/Console/Commands/AccountingInvoiceAdd.php @@ -0,0 +1,110 @@ +argument('siteid')); + Config::set('site',$site); + + $uo = User::where('email',$this->argument('user'))->singleOrFail(); + + $so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail(); + if (! ($to=$so->token($uo))) + abort(500,sprintf('Unknown Tokens for [%s]',$uo->email)); + + $o = Invoice::findOrFail($this->argument('id')); + + // Check the customer exists + if ($o->account->providers->where('pivot.provider_oauth_id',$so->id)->count() !== 1) + throw new \Exception('Account [%d] for Invoice [%d] not defined',$o->account_id,$o->id); + + $ao = $o->account->providers->where('pivot.provider_oauth_id',$so->id)->pop(); + + // Some validation + if (! $ao->pivot->ref) { + $this->error(sprintf('Accounting not defined for account [%d]',$o->account_id)); + exit(1); + } + + $acc = new AccInvoice; + $acc->CustomerRef = (object)['value'=>$ao->pivot->ref]; + $acc->DocNumber = $o->lid; + $acc->TxnDate = $o->created_at->format('Y-m-d'); + $acc->DueDate = $o->due_at->format('Y-m-d'); + + $lines = collect(); + $c = 0; + + // @todo Group these by ItemRef and the Same Unit Price and Description, so that we can then use quantity to represent the number of them. + foreach ($o->items->groupBy(function($item) use ($so) { + return sprintf('%s.%s.%s.%s',$item->item_type_name,$item->price_base,$item->product->provider_ref($so),$item->taxes->pluck('description')->join('|')); + }) as $os) + { + $key = $os->first(); + + // Some validation + if (! ($ref=$key->product->provider_ref($so))) { + $this->error(sprintf('Accounting not defined in product [%d]',$key->product_id)); + exit(1); + } + + if ($key->taxes->count() !== 1) { + $this->error(sprintf('Cannot handle when there is not just 1 tax line [%d]',$key->id)); + exit(1); + } + + $c++; + $line = new \stdClass; + $line->Id = $c; + $line->DetailType = 'SalesItemLineDetail'; + $line->Description = $key->item_type_name; + $line->SalesItemLineDetail = (object)[ + 'Qty' => $os->sum('quantity'), + 'UnitPrice' => $key->price_base, + 'ItemRef' => ['value'=>$ref], + // @todo It is assumed there is only 1 tax category + 'TaxCodeRef' => ['value'=>$key->taxes->first()->tax->provider_ref($so)], + ]; + $line->Amount = $os->sum('quantity')*$key->price_base; + + $lines->push($line); + } + + $acc->Line = $lines; + + return AccountingInvoiceUpdate::dispatchSync($to,$acc); + } +} \ No newline at end of file diff --git a/app/Console/Commands/AccountingInvoiceGet.php b/app/Console/Commands/AccountingInvoiceGet.php new file mode 100644 index 0000000..e7240cd --- /dev/null +++ b/app/Console/Commands/AccountingInvoiceGet.php @@ -0,0 +1,58 @@ +argument('siteid')); + Config::set('site',$site); + + $uo = User::where('email',$this->argument('user'))->singleOrFail(); + + $so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail(); + if (! ($to=$so->token($uo))) + abort(500,sprintf('Unknown Tokens for [%s]',$uo->email)); + + try { + $api = $to->API(); + dump($api->getInvoiceQuery($this->argument('id'))); + + } catch (ConnectException|ConnectionIssueException $e) { + $this->error($e->getMessage()); + + return Command::FAILURE; + } + } +} \ No newline at end of file diff --git a/app/Console/Commands/AccountingTaxSync.php b/app/Console/Commands/AccountingTaxSync.php new file mode 100644 index 0000000..155dc22 --- /dev/null +++ b/app/Console/Commands/AccountingTaxSync.php @@ -0,0 +1,51 @@ +argument('siteid')); + Config::set('site',$site); + + $uo = User::where('email',$this->argument('user'))->singleOrFail(); + + $so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail(); + if (! ($to=$so->token($uo))) + abort(500,sprintf('Unknown Tokens for [%s]',$uo->email)); + + Job::dispatchSync($to); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/AccountingController.php b/app/Http/Controllers/AccountingController.php index 5941b57..3a1db7b 100644 --- a/app/Http/Controllers/AccountingController.php +++ b/app/Http/Controllers/AccountingController.php @@ -24,20 +24,18 @@ class AccountingController extends Controller */ public static function list(string $provider): Collection { - $so = ProviderOauth::where('name',$provider)->singleOrFail(); // @todo This should be a variable $uo = User::findOrFail(1); - if (($x=$so->tokens->where('user_id',$uo->id))->count() !== 1) + $so = ProviderOauth::where('name',$provider)->singleOrFail(); + if (! ($to=$so->token($uo))) abort(500,sprintf('Unknown Tokens for [%s]',$uo->email)); - $to = $x->pop(); - - $api = $so->API($to,TRUE); // @todo Remove TRUE + $api = $to->API(); return $api->getItems() - ->pluck('pid','FullyQualifiedName') - ->transform(function($item,$value) { return ['id'=>$item,'value'=>$value]; }) + ->pluck('pid','Id') + ->transform(function($item,$value) { return ['id'=>$value,'value'=>$item]; }) ->values(); } diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index c8e4a9d..822b162 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -92,7 +92,7 @@ class ProductController extends Controller public function details_addedit(ProductAddEdit $request,Product $o) { - foreach ($request->except(['_token','submit','translate']) as $key => $item) + foreach ($request->except(['_token','submit','translate','accounting']) as $key => $item) $o->{$key} = $item; $o->active = (bool)$request->active; @@ -122,6 +122,15 @@ class ProductController extends Controller $o->translate()->save($oo); + if ($request->accounting) + foreach ($request->accounting as $k=>$v) + $o->providers()->syncWithoutDetaching([ + $k => [ + 'ref' => $v, + 'site_id'=>$o->site_id, + ], + ]); + return redirect()->back() ->with('success','Product saved'); } diff --git a/app/Http/Requests/ProductAddEdit.php b/app/Http/Requests/ProductAddEdit.php index 98846ca..54093a5 100644 --- a/app/Http/Requests/ProductAddEdit.php +++ b/app/Http/Requests/ProductAddEdit.php @@ -34,7 +34,7 @@ class ProductAddEdit extends FormRequest 'active' => 'sometimes|accepted', 'model' => 'sometimes|string', // @todo Check that it is a valid model type 'model_id' => 'sometimes|int', // @todo Check that it is a valid model type - 'accounting' => 'nullable|string', // @todo Validate that the value is in the accounting system + 'accounting' => 'nullable|array', // @todo Validate that the value is in the accounting system 'pricing' => 'required|array', // @todo Validate the elements in the pricing ]; } diff --git a/app/Jobs/AccountingAccountSync.php b/app/Jobs/AccountingAccountSync.php index e984306..a55131a 100644 --- a/app/Jobs/AccountingAccountSync.php +++ b/app/Jobs/AccountingAccountSync.php @@ -14,82 +14,94 @@ use Intuit\Jobs\AccountingCustomerUpdate; use App\Models\{Account,ProviderToken}; +/** + * Synchronise customers with our accounts. + * + * This will: + * + Create the account in the accounting system + * + Update the account in the accounting system with our data (we are master) + */ class AccountingAccountSync implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; private const LOGKEY = 'JAS'; private ProviderToken $to; - /** - * Create a new job instance. - * - * @return void - */ + /** + * Create a new job instance. + * + * @param ProviderToken $to + */ public function __construct(ProviderToken $to) { $this->to = $to; } - /** - * Execute the job. - * - * @return void - */ - public function handle() - { - $api = $this->to->provider->API($this->to); - $accounts = Account::with(['user'])->get(); + /** + * Execute the job. + * + * @return void + * @throws \Exception + */ + public function handle() + { + $api = $this->to->API(); + $ref = Account::select('id','site_id','company','user_id')->with(['user'])->get(); - foreach ($api->getCustomers() as $customer) { - $ao = NULL; + foreach ($api->getCustomers() as $acc) { + $o = NULL; // See if we are already linked - if (($x=$this->to->provider->accounts->where('pivot.ref',$customer->id))->count() === 1) { - $ao = $x->pop(); + if (($x=$this->to->provider->accounts->where('pivot.ref',$acc->id))->count() === 1) { + $o = $x->pop(); // If not, see if our reference matches - } elseif (($x=$accounts->filter(function($item) use ($customer) { return $item->sid == $customer->ref; }))->count() === 1) { - $ao = $x->pop(); + } elseif (($x=$ref->filter(function($item) use ($acc) { return $item->sid == $acc->ref; }))->count() === 1) { + $o = $x->pop(); // Look based on Name - } elseif (($x=$accounts->filter(function($item) use ($customer) { return $item->company == $customer->companyname || $item->name == $customer->fullname || $item->user->email == $customer->email; }))->count() === 1) { - $ao = $x->pop(); + } elseif (($x=$ref->filter(function($item) use ($acc) { return $item->company == $acc->companyname || $item->name == $acc->fullname || $item->user->email == $acc->email; }))->count() === 1) { + $o = $x->pop(); } else { // Log not found - Log::alert(sprintf('%s:Customer not found [%s:%s]',self::LOGKEY,$customer->id,$customer->DisplayName)); + Log::alert(sprintf('%s:Customer not found [%s:%s]',self::LOGKEY,$acc->id,$acc->DisplayName)); continue; } - $ao->providers()->syncWithoutDetaching([ + $o->providers()->syncWithoutDetaching([ $this->to->provider->id => [ - 'ref' => $customer->id, - 'synctoken' => $customer->synctoken, - 'created_at'=>Carbon::create($customer->created_at), - 'updated_at'=>Carbon::create($customer->updated_at), - 'site_id'=>$ao->site_id, // @todo See if we can have this handled automatically + 'ref' => $acc->id, + 'synctoken' => $acc->synctoken, + 'created_at'=>Carbon::create($acc->created_at), + 'updated_at'=>Carbon::create($acc->updated_at), + 'site_id'=>$this->to->site_id, ], ]); + Log::alert(sprintf('%s:Customer updated [%s:%s]',self::LOGKEY,$o->id,$acc->id)); + // Check if QB is out of Sync and update it. - $customer->syncOriginal(); - $customer->PrimaryEmailAddr = (object)['Address'=>$ao->user->email]; - $customer->ResaleNum = $ao->sid; - $customer->GivenName = $ao->user->firstname; - $customer->FamilyName = $ao->user->lastname; - $customer->CompanyName = $ao->name; - $customer->DisplayName = $ao->name; - $customer->FullyQualifiedName = $ao->name; - //$customer->Active = (bool)$ao->active; + $acc->syncOriginal(); + $acc->PrimaryEmailAddr = (object)['Address'=>$o->user->email]; + $acc->ResaleNum = $o->sid; + $acc->GivenName = $o->user->firstname; + $acc->FamilyName = $o->user->lastname; + $acc->CompanyName = $o->name; + $acc->DisplayName = $o->name; + $acc->FullyQualifiedName = $o->name; + //$acc->Active = (bool)$o->active; // @todo implement in-activity, but only if all invoices are paid and services cancelled - if ($customer->getDirty()) { - Log::info(sprintf('%s:Customer [%s] (%s:%s) has changed',self::LOGKEY,$ao->sid,$customer->id,$customer->DisplayName),['dirty'=>$customer->getDirty()]); - $customer->sparse = 'true'; + if ($acc->getDirty()) { + Log::info(sprintf('%s:Customer [%s] (%s:%s) has changed',self::LOGKEY,$o->sid,$acc->id,$acc->DisplayName),['dirty'=>$acc->getDirty()]); + $acc->sparse = 'true'; - AccountingCustomerUpdate::dispatch($this->to,$customer); + AccountingCustomerUpdate::dispatch($this->to,$acc); } + + // @todo Identify accounts in our DB that are not in accounting } - } + } } \ No newline at end of file diff --git a/app/Jobs/AccountingTaxSync.php b/app/Jobs/AccountingTaxSync.php new file mode 100644 index 0000000..ef44380 --- /dev/null +++ b/app/Jobs/AccountingTaxSync.php @@ -0,0 +1,86 @@ +to = $to; + } + + /** + * Execute the job. + * + * @return void + * @throws \Exception + */ + public function handle() + { + $api = $this->to->API(); + $ref = Tax::select(['id','description'])->get(); + + foreach ($api->getTaxCodes() as $acc) { + $o = NULL; + + // See if we are already linked + if (($x=$this->to->provider->taxes->where('pivot.ref',$acc->id))->count() === 1) { + $o = $x->pop(); + + /* + // If not, see if our reference matches + } elseif (($x=$ref->filter(function($item) use ($acc) { return $item->sid == $acc->ref; }))->count() === 1) { + $o = $x->pop(); + */ + + // Look based on Name + } elseif (($x=$ref->filter(function($item) use ($acc) { return $item->description === $acc->name; }))->count() === 1) { + $o = $x->pop(); + + } else { + // Log not found + Log::alert(sprintf('%s:Tax not found [%s:%s]',self::LOGKEY,$acc->id,$acc->name)); + continue; + } + + $o->providers()->syncWithoutDetaching([ + $this->to->provider->id => [ + 'ref' => $acc->id, + 'synctoken' => $acc->synctoken, + 'created_at'=>Carbon::create($acc->created_at), + 'updated_at'=>Carbon::create($acc->updated_at), + 'site_id'=>$this->to->site_id, + ], + ]); + + Log::alert(sprintf('%s:Tax updated [%s:%s]',self::LOGKEY,$o->id,$acc->id)); + } + } +} \ No newline at end of file diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 4443c95..ea8b90a 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -209,6 +209,13 @@ class Invoice extends Model implements IDs return $this->hasMany(PaymentItem::class); } + public function providers() + { + return $this->belongsToMany(ProviderOauth::class,'invoice__provider') + ->where('invoice__provider.site_id',$this->site_id) + ->withPivot('ref','synctoken','created_at','updated_at'); + } + /* SCOPES */ /** diff --git a/app/Models/InvoiceItemTax.php b/app/Models/InvoiceItemTax.php index 2030070..7bfa000 100644 --- a/app/Models/InvoiceItemTax.php +++ b/app/Models/InvoiceItemTax.php @@ -12,4 +12,11 @@ class InvoiceItemTax extends Model protected $table = 'invoice_item_taxes'; public $timestamps = FALSE; + + /* RELATIONS */ + + public function tax() + { + return $this->belongsTo(Tax::class); + } } \ No newline at end of file diff --git a/app/Models/Product.php b/app/Models/Product.php index 44cca1f..efcc59c 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -16,7 +16,7 @@ use Leenooks\Traits\ScopeActive; use App\Http\Controllers\AccountingController; use App\Interfaces\{IDs,ProductItem}; -use App\Traits\{ProductDetails,SiteID}; +use App\Traits\{ProductDetails,ProviderRef,SiteID}; /** * Class Product @@ -66,7 +66,7 @@ use App\Traits\{ProductDetails,SiteID}; */ class Product extends Model implements IDs { - use Compoships,HasFactory,SiteID,ProductDetails,ScopeActive; + use Compoships,HasFactory,SiteID,ProductDetails,ScopeActive,ProviderRef; protected $casts = [ 'pricing'=>'collection', @@ -106,6 +106,13 @@ class Product extends Model implements IDs /* RELATIONS */ + public function providers() + { + return $this->belongsToMany(ProviderOauth::class,'product__provider') + ->where('product__provider.site_id',$this->site_id) + ->withPivot('ref'); + } + /** * Which services are configured with this product * diff --git a/app/Models/ProviderOauth.php b/app/Models/ProviderOauth.php index f1be11a..41a14b2 100644 --- a/app/Models/ProviderOauth.php +++ b/app/Models/ProviderOauth.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use App\Traits\SiteID; @@ -23,10 +24,17 @@ class ProviderOauth extends Model ->withPivot('ref','synctoken','created_at','updated_at'); } - public function products() + public function invoices() { - return $this->belongsToMany(Product::class,'product__provider') - ->where('product__provider.site_id',$this->site_id) + return $this->belongsToMany(Invoice::class,'invoice__provider') + ->where('invoice__provider.site_id',$this->site_id) + ->withPivot('ref','synctoken','created_at','updated_at'); + } + + public function taxes() + { + return $this->belongsToMany(Tax::class,'tax__provider') + ->where('tax__provider.site_id',$this->site_id) ->withPivot('ref','synctoken','created_at','updated_at'); } @@ -42,23 +50,30 @@ class ProviderOauth extends Model /* METHODS */ + /** + * @return string|null + */ public function api_class(): ?string { return config('services.provider.'.strtolower($this->name).'.api'); } - public function API(ProviderToken $o,bool $tryprod=FALSE): mixed + /** + * Return a list of the provider OAUTH details + */ + public static function providers(): Collection { - return ($this->api_class() && $o->access_token) ? new ($this->api_class())($o,$tryprod) : NULL; + return (new self)::whereIn('name',array_keys(config('services.provider')))->get(); } /** - * Do we have API details for this supplier + * Return the token object for a specific user * - * @return bool + * @param User $uo + * @return ProviderToken|null */ - public function hasAPIdetails(): bool + public function token(User $uo): ?ProviderToken { - return $this->api_class() && $this->access_token && (! $this->hasAccessTokenExpired()); + return (($x=$this->tokens->where('user_id',$uo->id))->count() === 1) ? $x->pop() : NULL; } } \ No newline at end of file diff --git a/app/Models/ProviderToken.php b/app/Models/ProviderToken.php index 193f5a3..8c14196 100644 --- a/app/Models/ProviderToken.php +++ b/app/Models/ProviderToken.php @@ -15,10 +15,38 @@ class ProviderToken extends ProviderTokenBase 'refresh_token_expires_at', ]; + protected $with = ['provider']; + /* RELATIONS */ public function provider() { return $this->belongsTo(ProviderOauth::class,'provider_oauth_id'); } + + /* METHODS */ + + /** + * Return an API object to interact with this provider + * + * @return mixed + * @throws \Exception + */ + public function API(): mixed + { + if (! $this->provider->api_class() || ! $this->access_token) + throw new \Exception(sprintf('No API details for [%s]',$this->provider->name)); + + return new ($this->provider->api_class())($this); + } + + /** + * Do we have API details for this provider + * + * @return bool + */ + public function hasAPIdetails(): bool + { + return $this->provider->api_class() && $this->access_token && (! $this->hasAccessTokenExpired()); + } } \ No newline at end of file diff --git a/app/Models/Tax.php b/app/Models/Tax.php index 26ae743..d5a9a65 100644 --- a/app/Models/Tax.php +++ b/app/Models/Tax.php @@ -5,8 +5,12 @@ namespace App\Models; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use App\Traits\ProviderRef; + class Tax extends Model { + use ProviderRef; + public $timestamps = FALSE; /* RELATIONS */ @@ -16,6 +20,12 @@ class Tax extends Model return $this->belongsTo(Country::class); } + public function providers() + { + return $this->belongsToMany(ProviderOauth::class,'tax__provider') + ->withPivot('ref','synctoken','created_at','updated_at'); + } + /* METHODS */ /** diff --git a/app/Traits/ProviderRef.php b/app/Traits/ProviderRef.php new file mode 100644 index 0000000..014ab84 --- /dev/null +++ b/app/Traits/ProviderRef.php @@ -0,0 +1,16 @@ +providers->where('pivot.provider_oauth_id',$poo->id))->count() === 1) ? $x->pop()->pivot->ref : NULL; + } +} \ No newline at end of file diff --git a/config/services.php b/config/services.php index 6052581..06283cb 100644 --- a/config/services.php +++ b/config/services.php @@ -43,16 +43,16 @@ return [ 'redirect' => '/auth/google/callback', ], - 'provider' => [ - 'intuit' => [ - 'api'=> \Intuit\API::class, - ] - ], + 'provider' => [ + 'intuit' => [ + 'api'=> \Intuit\API::class, + ] + ], - 'supplier' => [ - 'crazydomain' => [ - 'api'=> \Dreamscape\API::class, - 'registrar' => 'crazydomain', // Key in the domain_registrars table - ], - ], + 'supplier' => [ + 'crazydomain' => [ + 'api'=> \Dreamscape\API::class, + 'registrar' => 'crazydomain', // Key in the domain_registrars table + ], + ], ]; diff --git a/database/migrations/2023_05_10_215107_accounting_invoice.php b/database/migrations/2023_05_10_215107_accounting_invoice.php new file mode 100644 index 0000000..b5df327 --- /dev/null +++ b/database/migrations/2023_05_10_215107_accounting_invoice.php @@ -0,0 +1,70 @@ +timestamps(); + $table->integer('invoice_id')->unsigned(); + $table->integer('provider_oauth_id')->unsigned(); + $table->integer('site_id')->unsigned(); + $table->string('ref'); + $table->integer('synctoken'); + + $table->foreign(['invoice_id','site_id'])->references(['id','site_id'])->on('invoices'); + $table->foreign(['provider_oauth_id','site_id'])->references(['id','site_id'])->on('provider_oauth'); + }); + + Schema::create('product__provider', function (Blueprint $table) { + $table->integer('product_id')->unsigned(); + $table->integer('provider_oauth_id')->unsigned(); + $table->integer('site_id')->unsigned(); + $table->string('ref'); + + $table->foreign(['product_id','site_id'])->references(['id','site_id'])->on('products'); + $table->foreign(['provider_oauth_id','site_id'])->references(['id','site_id'])->on('provider_oauth'); + }); + + Schema::create('tax__provider', function (Blueprint $table) { + $table->timestamps(); + $table->integer('tax_id')->unsigned(); + $table->integer('provider_oauth_id')->unsigned(); + $table->integer('site_id')->unsigned(); + $table->string('ref'); + $table->integer('synctoken'); + + $table->foreign(['tax_id'])->references(['id'])->on('taxes'); + $table->foreign(['provider_oauth_id','site_id'])->references(['id','site_id'])->on('provider_oauth'); + }); + + Schema::table('products', function (Blueprint $table) { + $table->dropColumn('accounting'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('tax__provider'); + Schema::dropIfExists('product__provider'); + Schema::dropIfExists('invoice__provider'); + + Schema::table('products', function (Blueprint $table) { + $table->string('accounting'); + }); + } +}; diff --git a/resources/views/theme/backend/adminlte/product/widget/detail.blade.php b/resources/views/theme/backend/adminlte/product/widget/detail.blade.php index fb33b9b..4c3ad01 100644 --- a/resources/views/theme/backend/adminlte/product/widget/detail.blade.php +++ b/resources/views/theme/backend/adminlte/product/widget/detail.blade.php @@ -105,15 +105,29 @@
- @include('adminlte::widget.form_select',[ - 'label'=>'Accounting', - 'icon'=>'fas fa-calculator', - 'id'=>'accounting', - 'old'=>'accounting', - 'name'=>'accounting', - 'value'=>$o->accounting, - 'options'=>$o->accounting('intuit'), - ]) + Accounting +
+ @foreach (\App\Models\ProviderOauth::providers() as $apo) +
+ +
+
+ +
+ + + @error($x) + {{ $message }} + @enderror + +
+
+ @endforeach
@@ -121,6 +135,9 @@
Pricing Ex Taxes
+ @error('pricing') + @include('adminlte::widget.errors') + @enderror