Inuit sync of tax, product accounting, accounts and invoices
This commit is contained in:
parent
e2d8f8a096
commit
ad2f6f3a7f
@ -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);
|
||||
}
|
||||
}
|
58
app/Console/Commands/AccountingAccountGet.php
Normal file
58
app/Console/Commands/AccountingAccountGet.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
110
app/Console/Commands/AccountingInvoiceAdd.php
Normal file
110
app/Console/Commands/AccountingInvoiceAdd.php
Normal 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);
|
||||
}
|
||||
}
|
58
app/Console/Commands/AccountingInvoiceGet.php
Normal file
58
app/Console/Commands/AccountingInvoiceGet.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
51
app/Console/Commands/AccountingTaxSync.php
Normal file
51
app/Console/Commands/AccountingTaxSync.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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
|
||||
];
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
86
app/Jobs/AccountingTaxSync.php
Normal file
86
app/Jobs/AccountingTaxSync.php
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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 */
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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 */
|
||||
|
||||
/**
|
||||
|
16
app/Traits/ProviderRef.php
Normal file
16
app/Traits/ProviderRef.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
],
|
||||
],
|
||||
];
|
||||
|
70
database/migrations/2023_05_10_215107_accounting_invoice.php
Normal file
70
database/migrations/2023_05_10_215107_accounting_invoice.php
Normal 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');
|
||||
});
|
||||
}
|
||||
};
|
@ -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
|
||||
|
@ -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> </th>
|
||||
<td class="text-center" colspan="2">#{{ $p->supplied->id }}: {{ $p->supplied->name_long }}</td>
|
||||
|
Loading…
Reference in New Issue
Block a user