Inuit sync of tax, product accounting, accounts and invoices

This commit is contained in:
Deon George 2023-05-12 20:09:51 +10:00
parent e2d8f8a096
commit ad2f6f3a7f
22 changed files with 707 additions and 144 deletions

View File

@ -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);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use GuzzleHttp\Exception\ConnectException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Intuit\Exceptions\ConnectionIssueException;
use App\Models\{ProviderOauth,Site,User};
class AccountingAccountGet extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:account:get'
.' {siteid : Site ID}'
.' {provider : Provider Name}'
.' {user : User Email}'
.' {id : Account ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get an account from the accounting provider';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->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;
}
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Intuit\Jobs\AccountingInvoiceUpdate;
use Intuit\Models\Invoice as AccInvoice;
use App\Models\{Invoice,ProviderOauth,Site,User};
class AccountingInvoiceAdd extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:invoice:add'
.' {siteid : Site ID}'
.' {provider : Provider Name}'
.' {user : User Email}'
.' {id : Invoice ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add an invoice to the accounting provider';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->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);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use GuzzleHttp\Exception\ConnectException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Intuit\Exceptions\ConnectionIssueException;
use App\Models\{ProviderOauth,Site,User};
class AccountingInvoiceGet extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:invoice:get'
.' {siteid : Site ID}'
.' {provider : Provider Name}'
.' {user : User Email}'
.' {id : Invoice ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get an invoice from the accounting provider';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->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;
}
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use App\Models\{ProviderOauth,Site,User};
use App\Jobs\AccountingTaxSync as Job;
/**
* Synchronise TAX ids with our taxes.
*/
class AccountingTaxSync extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:tax:sync'
.' {siteid : Site ID}'
.' {provider : Provider Name}'
.' {user : User Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronise taxes with accounting system';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->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);
}
}

View File

@ -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();
}

View File

@ -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');
}

View File

@ -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
];
}

View File

@ -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
}
}
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Jobs;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use App\Models\{Tax,ProviderToken};
/**
* Synchronise TAX ids with our taxes.
*
* This will only update our records, it wont create new records in the account system, nor in our DB
*/
class AccountingTaxSync implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private const LOGKEY = 'JTS';
private ProviderToken $to;
/**
* Create a new job instance.
*
* @param ProviderToken $to
*/
public function __construct(ProviderToken $to)
{
$this->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));
}
}
}

View File

@ -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 */
/**

View File

@ -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);
}
}

View File

@ -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
*

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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 */
/**

View File

@ -0,0 +1,16 @@
<?php
/**
* Get a provider reference ID
*/
namespace App\Traits;
use App\Models\ProviderOauth;
trait ProviderRef
{
public function provider_ref(ProviderOauth $poo): ?string
{
return (($x=$this->providers->where('pivot.provider_oauth_id',$poo->id))->count() === 1) ? $x->pop()->pivot->ref : NULL;
}
}

View File

@ -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
],
],
];

View File

@ -0,0 +1,70 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('invoice__provider', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -105,15 +105,29 @@
<div class="row">
<!-- Accounting -->
<div class="col-12">
@include('adminlte::widget.form_select',[
'label'=>'Accounting',
'icon'=>'fas fa-calculator',
'id'=>'accounting',
'old'=>'accounting',
'name'=>'accounting',
'value'=>$o->accounting,
'options'=>$o->accounting('intuit'),
])
<span class="h5">Accounting</span>
<hr>
@foreach (\App\Models\ProviderOauth::providers() as $apo)
<div class="form-group">
<label for="{{ $x=sprintf('acc_%d',$apo->id) }}">{{ ucfirst($apo->name) }}</label>
<div class="input-group has-validation">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa-fw fas fa-calculator"></i></span>
</div>
<select class="form-control @error($x) is-invalid @enderror select" id="{{ $x }}" name="{{ sprintf('accounting[%d]',$apo->id) }}">
<option></option>
@foreach ($o->accounting($apo->name) as $v)
<option value="{{ $v['id'] }}" @if($o->provider_ref($apo) === (string)$v['id'])selected @endif>{{ $v['value'] }}</option>
@endforeach
</select>
<span class="invalid-feedback" role="alert">
@error($x)
{{ $message }}
@enderror
</span>
</div>
</div>
@endforeach
</div>
</div>
</div>
@ -121,6 +135,9 @@
<div class="col-12 offset-md-3 col-md-5">
<span class="h5">Pricing</span><small> Ex Taxes</small>
<hr>
@error('pricing')
@include('adminlte::widget.errors')
@enderror
<ul class="nav nav-pills w-100 pl-0 pt-2 pb-2">
@foreach(\App\Models\Group::pricing()->active()->get() as $go)
@ -158,7 +175,7 @@
<div class="col-6">
<div class="form-group">
<label for="{{ $x=sprintf('setup_%d_%d',$bp,$go->id) }}">{{ $detail['name'] }} Reoccurring</label>
<label for="{{ $x=sprintf('setup_%d_%d',$bp,$go->id) }}">{{ $detail['name'] }} Setup</label>
<div class="input-group has-validation">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa-fw fas fa-cog"></i></span>
@ -242,6 +259,7 @@
});
$('#model_id').select2();
$('.select').select2();
// After we render the page, hide the supplier_product if this product has no model.
// We do this here, because adding d-none to the div results in the select2 input not presenting correctly

View File

@ -27,7 +27,7 @@
<tbody>
<tr>
<th>Product</th>
<td class="text-center" colspan="2">#{{ $c->supplied->id }}: {{ $c->supplied->name_long }}</td>
<td class="text-center" colspan="2"><a href="{{ url('a/product/details',$c->supplied->id) }}">#{{ $c->supplied->id }}: {{ $c->supplied->name_long }}</a></td>
@if ($p->exists)
<th>&nbsp;</th>
<td class="text-center" colspan="2">#{{ $p->supplied->id }}: {{ $p->supplied->name_long }}</td>