From b067697b1ca1e84a031705757af90a31e7779c8a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 15 May 2016 13:58:11 +0300 Subject: [PATCH] Support manually importing OFX files --- app/Http/Controllers/AccountController.php | 14 +-- .../Controllers/BankAccountController.php | 26 ++++++ app/Http/Controllers/BaseController.php | 2 +- app/Http/routes.php | 26 +++--- app/Libraries/OFX.php | 10 +- app/Models/Expense.php | 15 ++- app/Services/BankAccountService.php | 92 ++++++++++++------- resources/lang/en/texts.php | 4 +- .../views/accounts/bank_account.blade.php | 31 ++++--- resources/views/accounts/banks.blade.php | 40 ++++---- resources/views/accounts/import_ofx.blade.php | 26 ++++++ 11 files changed, 187 insertions(+), 99 deletions(-) create mode 100644 resources/views/accounts/import_ofx.blade.php diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 97226ad29c2a..3abaaadfd78c 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -402,17 +402,9 @@ class AccountController extends BaseController private function showBankAccounts() { - $account = Auth::user()->account; - $account->load('bank_accounts'); - $count = count($account->bank_accounts); - - if ($count == 0) { - return Redirect::to('bank_accounts/create'); - } else { - return View::make('accounts.banks', [ - 'title' => trans('texts.bank_accounts') - ]); - } + return View::make('accounts.banks', [ + 'title' => trans('texts.bank_accounts') + ]); } private function showOnlinePayments() diff --git a/app/Http/Controllers/BankAccountController.php b/app/Http/Controllers/BankAccountController.php index bce86bce42f1..1c003cfd47b0 100644 --- a/app/Http/Controllers/BankAccountController.php +++ b/app/Http/Controllers/BankAccountController.php @@ -13,12 +13,14 @@ use stdClass; use Crypt; use URL; use Utils; +use File; use App\Models\Gateway; use App\Models\Account; use App\Models\BankAccount; use App\Ninja\Repositories\BankAccountRepository; use App\Services\BankAccountService; use App\Http\Requests\CreateBankAccountRequest; +use Illuminate\Http\Request; class BankAccountController extends BaseController { @@ -122,4 +124,28 @@ class BankAccountController extends BaseController return $this->bankAccountService->importExpenses($bankId, Input::all()); } + public function showImportOFX() + { + return view('accounts.import_ofx'); + } + + public function doImportOFX(Request $request) + { + $file = File::get($request->file('ofx_file')); + + try { + $data = $this->bankAccountService->parseOFX($file); + } catch (\Exception $e) { + Session::flash('error', trans('texts.ofx_parse_failed')); + return view('accounts.import_ofx'); + } + + $data = [ + 'banks' => null, + 'bankAccount' => null, + 'transactions' => json_encode([$data]) + ]; + + return View::make('accounts.bank_account', $data); + } } diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 2ce7a633f179..3bb399294c14 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -10,7 +10,7 @@ use Utils; class BaseController extends Controller { use DispatchesJobs, AuthorizesRequests; - + protected $entityType; /** diff --git a/app/Http/routes.php b/app/Http/routes.php index 1d6d6b8fb903..47b1a28b1c4e 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -60,7 +60,7 @@ Route::group(['middleware' => 'auth:client'], function() { Route::get('client/documents/js/{documents}/{filename}', 'PublicClientController@getDocumentVFSJS'); Route::get('client/documents/{invitation_key}/{documents}/{filename?}', 'PublicClientController@getDocument'); Route::get('client/documents/{invitation_key}/{filename?}', 'PublicClientController@getInvoiceDocumentsZip'); - + Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'PublicClientController@quoteDatatable')); Route::get('api/client.invoices', array('as'=>'api.client.invoices', 'uses'=>'PublicClientController@invoiceDatatable')); Route::get('api/client.recurring_invoices', array('as'=>'api.client.recurring_invoices', 'uses'=>'PublicClientController@recurringInvoiceDatatable')); @@ -123,7 +123,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('hide_message', 'HomeController@hideMessage'); Route::get('force_inline_pdf', 'UserController@forcePDFJS'); Route::get('account/getSearchData', array('as' => 'getSearchData', 'uses' => 'AccountController@getSearchData')); - + Route::get('settings/user_details', 'AccountController@showUserDetails'); Route::post('settings/user_details', 'AccountController@saveUserDetails'); Route::post('users/change_password', 'UserController@changePassword'); @@ -156,7 +156,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('documents/js/{documents}/{filename}', 'DocumentController@getVFSJS'); Route::get('documents/preview/{documents}/{filename?}', 'DocumentController@getPreview'); Route::post('document', 'DocumentController@postUpload'); - + Route::get('quotes/create/{client_id?}', 'QuoteController@create'); Route::get('quotes/{invoices}/clone', 'InvoiceController@cloneInvoice'); Route::get('quotes/{invoices}/edit', 'InvoiceController@edit'); @@ -245,6 +245,8 @@ Route::group([ Route::get('api/gateways', array('as'=>'api.gateways', 'uses'=>'AccountGatewayController@getDatatable')); Route::post('account_gateways/bulk', 'AccountGatewayController@bulk'); + Route::get('bank_accounts/import_ofx', 'BankAccountController@showImportOFX'); + Route::post('bank_accounts/import_ofx', 'BankAccountController@doImportOFX'); Route::resource('bank_accounts', 'BankAccountController'); Route::get('api/bank_accounts', array('as'=>'api.bank_accounts', 'uses'=>'BankAccountController@getDatatable')); Route::post('bank_accounts/bulk', 'BankAccountController@bulk'); @@ -487,7 +489,7 @@ if (!defined('CONTACT_EMAIL')) { define('INVOICE_STATUS_APPROVED', 4); define('INVOICE_STATUS_PARTIAL', 5); define('INVOICE_STATUS_PAID', 6); - + define('PAYMENT_STATUS_PENDING', 1); define('PAYMENT_STATUS_VOIDED', 2); define('PAYMENT_STATUS_FAILED', 3); @@ -706,7 +708,7 @@ if (!defined('CONTACT_EMAIL')) { define('AUTO_BILL_OPT_IN', 1); define('AUTO_BILL_OPT_OUT', 2); define('AUTO_BILL_ALWAYS', 3); - + // These must be lowercase define('PLAN_FREE', 'free'); define('PLAN_PRO', 'pro'); @@ -714,7 +716,7 @@ if (!defined('CONTACT_EMAIL')) { define('PLAN_WHITE_LABEL', 'white_label'); define('PLAN_TERM_MONTHLY', 'month'); define('PLAN_TERM_YEARLY', 'year'); - + // Pro define('FEATURE_CUSTOMIZE_INVOICE_DESIGN', 'customize_invoice_design'); define('FEATURE_REMOVE_CREATED_BY', 'remove_created_by'); @@ -729,23 +731,23 @@ if (!defined('CONTACT_EMAIL')) { define('FEATURE_API', 'api'); define('FEATURE_CLIENT_PORTAL_PASSWORD', 'client_portal_password'); define('FEATURE_CUSTOM_URL', 'custom_url'); - + define('FEATURE_MORE_CLIENTS', 'more_clients'); // No trial allowed - + // Whitelabel define('FEATURE_CLIENT_PORTAL_CSS', 'client_portal_css'); define('FEATURE_WHITE_LABEL', 'feature_white_label'); // Enterprise define('FEATURE_DOCUMENTS', 'documents'); - + // No Trial allowed define('FEATURE_USERS', 'users');// Grandfathered for old Pro users define('FEATURE_USER_PERMISSIONS', 'user_permissions'); - + // Pro users who started paying on or before this date will be able to manage users define('PRO_USERS_GRANDFATHER_DEADLINE', '2016-05-15'); - + $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], @@ -801,4 +803,4 @@ if (Utils::isNinjaDev()) //ini_set('memory_limit','1024M'); //Auth::loginUsingId(1); } -*/ \ No newline at end of file +*/ diff --git a/app/Libraries/OFX.php b/app/Libraries/OFX.php index 721c9f529f85..b32e308a4247 100644 --- a/app/Libraries/OFX.php +++ b/app/Libraries/OFX.php @@ -27,15 +27,15 @@ class OFX curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-Type: application/x-ofx', 'User-Agent: httpclient')); curl_setopt($c, CURLOPT_POSTFIELDS, $this->request); curl_setopt($c, CURLOPT_RETURNTRANSFER, 1); - + $this->response = curl_exec($c); - + if (Utils::isNinjaDev()) { Log::info(print_r($this->response, true)); } - + curl_close($c); - + $tmp = explode('', $this->response); $this->responseHeader = $tmp[0]; $this->responseBody = ''.$tmp[1]; @@ -48,6 +48,7 @@ class OFX return $x; } + public static function closeTags($x) { $x = preg_replace('/\s+/', '', $x); @@ -233,4 +234,3 @@ class Account } } } - diff --git a/app/Models/Expense.php b/app/Models/Expense.php index 316491a5356b..e626e08549bb 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -90,15 +90,24 @@ class Expense extends EntityModel { return round($this->amount * $this->exchange_rate, 2); } - + public function toArray() { $array = parent::toArray(); - + if(empty($this->visible) || in_array('converted_amount', $this->visible))$array['converted_amount'] = $this->convertedAmount(); - + return $array; } + + public function scopeBankId($query, $bankdId = null) + { + if ($bankdId) { + $query->whereBankId($bankId); + } + + return $query; + } } Expense::creating(function ($expense) { diff --git a/app/Services/BankAccountService.php b/app/Services/BankAccountService.php index 72aada6e3ff9..c04651629856 100644 --- a/app/Services/BankAccountService.php +++ b/app/Services/BankAccountService.php @@ -34,14 +34,10 @@ class BankAccountService extends BaseService return $this->bankAccountRepo; } - public function loadBankAccounts($bankId, $username, $password, $includeTransactions = true) + private function getExpenses($bankId = null) { - if (! $bankId || ! $username || ! $password) { - return false; - } - $expenses = Expense::scope() - ->whereBankId($bankId) + ->bankId($bankId) ->where('transaction_id', '!=', '') ->withTrashed() ->get(['transaction_id']) @@ -50,6 +46,16 @@ class BankAccountService extends BaseService return $val['transaction_id']; }, $expenses)); + return $expenses; + } + + public function loadBankAccounts($bankId, $username, $password, $includeTransactions = true) + { + if (! $bankId || ! $username || ! $password) { + return false; + } + + $expenses = $this->getExpenses(); $vendorMap = $this->createVendorMap(); $bankAccounts = BankSubaccount::scope() ->whereHas('bank_account', function ($query) use ($bankId) { @@ -106,44 +112,60 @@ class BankAccountService extends BaseService $obj->balance = Utils::formatMoney($account->ledgerBalance, CURRENCY_DOLLAR); if ($includeTransactions) { - $ofxParser = new \OfxParser\Parser(); - $ofx = $ofxParser->loadFromString($account->response); - - $obj->start_date = $ofx->BankAccount->Statement->startDate; - $obj->end_date = $ofx->BankAccount->Statement->endDate; - $obj->transactions = []; - - foreach ($ofx->BankAccount->Statement->transactions as $transaction) { - // ensure transactions aren't imported as expenses twice - if (isset($expenses[$transaction->uniqueId])) { - continue; - } - if ($transaction->amount >= 0) { - continue; - } - - // if vendor has already been imported use current name - $vendorName = trim(substr($transaction->name, 0, 20)); - $key = strtolower($vendorName); - $vendor = isset($vendorMap[$key]) ? $vendorMap[$key] : null; - - $transaction->vendor = $vendor ? $vendor->name : $this->prepareValue($vendorName); - $transaction->info = $this->prepareValue(substr($transaction->name, 20)); - $transaction->memo = $this->prepareValue($transaction->memo); - $transaction->date = \Auth::user()->account->formatDate($transaction->date); - $transaction->amount *= -1; - $obj->transactions[] = $transaction; - } + $obj = $this->parseTransactions($obj, $account->response, $expenses, $vendorMap); } return $obj; } + private function parseTransactions($account, $data, $expenses, $vendorMap) + { + $ofxParser = new \OfxParser\Parser(); + $ofx = $ofxParser->loadFromString($data); + + $account->start_date = $ofx->BankAccount->Statement->startDate; + $account->end_date = $ofx->BankAccount->Statement->endDate; + $account->transactions = []; + + foreach ($ofx->BankAccount->Statement->transactions as $transaction) { + // ensure transactions aren't imported as expenses twice + if (isset($expenses[$transaction->uniqueId])) { + continue; + } + if ($transaction->amount >= 0) { + continue; + } + + // if vendor has already been imported use current name + $vendorName = trim(substr($transaction->name, 0, 20)); + $key = strtolower($vendorName); + $vendor = isset($vendorMap[$key]) ? $vendorMap[$key] : null; + + $transaction->vendor = $vendor ? $vendor->name : $this->prepareValue($vendorName); + $transaction->info = $this->prepareValue(substr($transaction->name, 20)); + $transaction->memo = $this->prepareValue($transaction->memo); + $transaction->date = \Auth::user()->account->formatDate($transaction->date); + $transaction->amount *= -1; + $account->transactions[] = $transaction; + } + + return $account; + } + private function prepareValue($value) { return ucwords(strtolower(trim($value))); } + public function parseOFX($data) + { + $account = new stdClass; + $expenses = $this->getExpenses(); + $vendorMap = $this->createVendorMap(); + + return $this->parseTransactions($account, $data, $expenses, $vendorMap); + } + private function createVendorMap() { $vendorMap = []; @@ -158,7 +180,7 @@ class BankAccountService extends BaseService return $vendorMap; } - public function importExpenses($bankId, $input) + public function importExpenses($bankId = 0, $input) { $vendorMap = $this->createVendorMap(); $countVendors = 0; diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 5ccec0032495..51f746402a87 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1295,7 +1295,9 @@ $LANG = array( 'no_payment_method_specified' => 'No payment method specified', 'chart_type' => 'Chart Type', 'format' => 'Format', - + 'import_ofx' => 'Import OFX', + 'ofx_file' => 'OFX File', + 'ofx_parse_failed' => 'Failed to parse OFX file', ); return $LANG; diff --git a/resources/views/accounts/bank_account.blade.php b/resources/views/accounts/bank_account.blade.php index ff1a0affef2e..4483bc30252e 100644 --- a/resources/views/accounts/bank_account.blade.php +++ b/resources/views/accounts/bank_account.blade.php @@ -2,7 +2,7 @@ @section('head') @parent - + @include('money_script')