From 24bcb0e2e58e671ad1d06e66bce29db31450cdf9 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 17 Nov 2015 15:53:14 +0200 Subject: [PATCH] Merging import changes --- app/Http/Controllers/AccountController.php | 31 +++- app/Http/routes.php | 2 + .../Import/DataImporterServiceInterface.php | 15 ++ .../Import/FreshBooks/ClientTransformer.php | 107 ++++++++++++ .../FreshBooksDataImporterService.php | 154 ++++++++++++++++++ .../Import/FreshBooks/InvoiceTransformer.php | 103 ++++++++++++ .../Import/FreshBooks/StaffTransformer.php | 88 ++++++++++ .../FreshBooks/TimesheetTransformer.php | 68 ++++++++ app/Providers/AppServiceProvider.php | 5 + resources/lang/en/texts.php | 16 +- .../views/accounts/import_export.blade.php | 14 ++ resources/views/accounts/import_map.blade.php | 2 +- 12 files changed, 598 insertions(+), 7 deletions(-) create mode 100644 app/Ninja/Import/DataImporterServiceInterface.php create mode 100644 app/Ninja/Import/FreshBooks/ClientTransformer.php create mode 100644 app/Ninja/Import/FreshBooks/FreshBooksDataImporterService.php create mode 100644 app/Ninja/Import/FreshBooks/InvoiceTransformer.php create mode 100644 app/Ninja/Import/FreshBooks/StaffTransformer.php create mode 100644 app/Ninja/Import/FreshBooks/TimesheetTransformer.php diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 840e8f5540ce..56b698b6d8a0 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -1,6 +1,7 @@ userMailer = $userMailer; $this->contactMailer = $contactMailer; $this->referralRepository = $referralRepository; + $this->clientRepository = $clientRepository; + $this->dataImporterService = $dataImporterService; } public function demo() @@ -413,6 +417,8 @@ class AccountController extends BaseController return AccountController::saveLocalization(); } elseif ($section === ACCOUNT_IMPORT_EXPORT) { return AccountController::importFile(); + } elseif ($section === IMPORT_FROM_FRESHBOOKS) { + return AccountController::importData(); } elseif ($section === ACCOUNT_MAP) { return AccountController::mapFile(); } elseif ($section === ACCOUNT_NOTIFICATIONS) { @@ -721,8 +727,7 @@ class AccountController extends BaseController continue; } - $clientRepository = new ClientRepository(); - $clientRepository->save($data); + $this->clientRepository->save($data); $count++; } @@ -732,6 +737,26 @@ class AccountController extends BaseController return Redirect::to('clients'); } + private function importData() + { + try + { + $files['client'] = Input::file('client_file'); + $files['invoice'] = Input::file('invoice_file'); + $files['timesheet'] = Input::file('timesheet_file'); + $imported_files = $this->dataImporterService->import($files); + } + catch(Exception $e) + { + Session::flash('error', $e->getMessage()); + return Redirect::to('settings/' . ACCOUNT_IMPORT_EXPORT); + } + + Session::flash('message', trans('texts.imported_file').' - '.$imported_files); + + return Redirect::to('settings/' . ACCOUNT_IMPORT_EXPORT); + } + private function mapFile() { $file = Input::file('file'); diff --git a/app/Http/routes.php b/app/Http/routes.php index 71cfbc898401..16826fc7e7fe 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -337,6 +337,8 @@ if (!defined('CONTACT_EMAIL')) { define('DEFAULT_FONT_SIZE', 9); define('DEFAULT_SEND_RECURRING_HOUR', 8); + define('IMPORT_FROM_FRESHBOOKS', 'import_from_freshbook'); + define('MAX_NUM_CLIENTS', 100); define('MAX_NUM_CLIENTS_PRO', 20000); define('MAX_NUM_CLIENTS_LEGACY', 500); diff --git a/app/Ninja/Import/DataImporterServiceInterface.php b/app/Ninja/Import/DataImporterServiceInterface.php new file mode 100644 index 000000000000..7d87306fd7ac --- /dev/null +++ b/app/Ninja/Import/DataImporterServiceInterface.php @@ -0,0 +1,15 @@ +arrayToObject($data); + return [ + 'name' => $data->organization !== array() ? $data->organization : '', + 'work_phone' => $data->busPhone !== array() ? $data->busPhone : '', + 'address1' => $data->street !== array() ? $data->street : '', + 'address2' => $data->street2 !== array() ? $data->street2 : '', + 'city' => $data->city !== array() ? $data->city : '', + 'state' => $data->province !== array() ? $data->province : '', + 'postal_code' => $data->postalCode !== array() ? $data->postalCode : '', + 'private_notes' => $data->notes !== array() ? $data->notes : '', + 'contacts' => [ + [ + 'public_id' => '', + 'first_name' => $data->firstName !== array() ? $data->firstName : '', + 'last_name' => $data->lastName !== array() ? $data->lastName : '', + 'email' => $data->email !== array() ? $data->email : '', + 'phone' => $data->mobPhone !== array() ? $data->mobPhone : $data->homePhone, + ] + ], + 'country_id' => !Country::where('name', $data->country) + ->get() + ->isEmpty() ? Country::where('name', $data->country) + ->first()->id : null, + ]; + }); + } + + private function arrayToObject($array) + { + $object = new stdClass(); + $object->organization = $array[0]; + $object->firstName = $array[1]; + $object->lastName = $array[2]; + $object->email = $array[3]; + $object->street = $array[4]; + $object->street2 = $array[5]; + $object->city = $array[6]; + $object->province = $array[7]; + $object->country = $array[8]; + $object->postalCode = $array[9]; + $object->busPhone = $array[10]; + $object->homePhone = $array[11]; + $object->mobPhone = $array[12]; + $object->fax = $array[13]; + $object->secStreet = $array[14]; + $object->secStreet2 = $array[15]; + $object->secCity = $array[16]; + $object->secProvince = $array[17]; + $object->secCountry = $array[18]; + $object->secPostalCode = $array[19]; + $object->notes = $array[20]; + return $object; + } + + public function validateHeader($csvHeader) + { + $header = [0 => "Organization", + 1 => "FirstName", + 2 => "LastName", + 3 => "Email", + 4 => "Street", + 5 => "Street2", + 6 => "City", + 7 => "Province", + 8 => "Country", + 9 => "PostalCode", + 10 => "BusPhone", + 11 => "HomePhone", + 12 => "MobPhone", + 13 => "Fax", + 14 => "SecStreet", + 15 => "SecStreet2", + 16 => "SecCity", + 17 => "SecProvince", + 18 => "SecCountry", + 19 => "SecPostalCode", + 20 => "Notes"]; + + if(!empty(array_diff($header, $csvHeader))) + throw new Exception(trans('texts.invalid_csv_header')); + } + + +} \ No newline at end of file diff --git a/app/Ninja/Import/FreshBooks/FreshBooksDataImporterService.php b/app/Ninja/Import/FreshBooks/FreshBooksDataImporterService.php new file mode 100644 index 000000000000..caa8370945a0 --- /dev/null +++ b/app/Ninja/Import/FreshBooks/FreshBooksDataImporterService.php @@ -0,0 +1,154 @@ +clientRepo = $clientRepo; + $this->invoiceRepo = $invoiceRepo; + $this->container = $container; + + $this->fractal = $manager; + $this->transformerList = array( + 'client' => __NAMESPACE__ . '\ClientTransformer', + 'invoice' => __NAMESPACE__ . '\InvoiceTransformer', + 'timesheet' => __NAMESPACE__ . '\TimesheetTransformer', + ); + + $this->repositoryList = array( + 'client' => '\App\Ninja\Repositories\ClientRepository', + 'invoice' => '\App\Ninja\Repositories\InvoiceRepository', + 'timesheet' => '\App\Ninja\Repositories\TaskRepository', + ); + } + + public function import($files) + { + $imported_files = null; + + foreach($files as $entity => $file) + { + $imported_files = $imported_files . $this->execute($entity, $file); + } + return $imported_files; + } + + private function execute($entity, $file) + { + $this->transformer = $this->createTransformer($entity); + $this->repository = $this->createRepository($entity); + + $data = $this->parseCSV($file); + $ignore_header = true; + try + { + $rows = $this->mapCsvToModel($data, $ignore_header); + } catch(Exception $e) + { + throw new Exception($e->getMessage() . ' - ' . $file->getClientOriginalName() ); + } + + $errorMessages = null; + + foreach($rows as $row) + { + if($entity=='timesheet') + { + $publicId = false; + $this->repository->save($publicId, $row); + } else { + $this->repository->save($row); + } + } + + + return $file->getClientOriginalName().' '.$errorMessages; + } + + private function parseCSV($file) + { + if ($file == null) + throw new Exception(trans('texts.select_file')); + + $name = $file->getRealPath(); + + require_once app_path().'/Includes/parsecsv.lib.php'; + $csv = new parseCSV(); + $csv->heading = false; + $csv->auto($name); + + //Review this code later. Free users can only have 100 clients. + /* + if (count($csv->data) + Client::scope()->count() > Auth::user()->getMaxNumClients()) { + $message = trans('texts.limit_clients', ['count' => Auth::user()->getMaxNumClients()]); + } + */ + + return $csv->data; + } + + /** + * @param $data + * Header of the Freshbook CSV File + + * @param $ignore_header + * @return mixed + */ + private function mapCsvToModel($data, $ignore_header) + { + if($ignore_header) + { + $header = array_shift($data); + $this->transformer->validateHeader($header); + } + + $resource = $this->transformer->transform($data); + $data = $this->fractal->createData($resource)->toArray(); + + return $data['data']; + } + + public function createTransformer($type) + { + if (!array_key_exists($type, $this->transformerList)) { + throw new \InvalidArgumentException("$type is not a valid Transformer"); + } + $className = $this->transformerList[$type]; + return new $className(); + } + + public function createRepository($type) + { + if (!array_key_exists($type, $this->repositoryList)) { + throw new \InvalidArgumentException("$type is not a valid Repository"); + } + $className = $this->repositoryList[$type]; + return $this->container->make($className); + //return new $className(); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/FreshBooks/InvoiceTransformer.php b/app/Ninja/Import/FreshBooks/InvoiceTransformer.php new file mode 100644 index 000000000000..2e8519ce7fb0 --- /dev/null +++ b/app/Ninja/Import/FreshBooks/InvoiceTransformer.php @@ -0,0 +1,103 @@ +arrayToObject($data); + $client = Client::where('name', $data->organization)->orderBy('created_at', 'desc')->first(); + $data->client_id = $client->id; + $data->user_id = $client->user_id; + $data->account_id = $client->account_id; + $create_date = new \DateTime($data->create_date); + $data->create_date = date_format($create_date, DEFAULT_DATE_FORMAT); + return [ + 'invoice_number' => $data->invoice_number !== array() ? $data->invoice_number : '', + 'client_id' => (int)$data->client_id !== array() ? $data->client_id : '', + 'user_id' => (int)$data->user_id !== array() ? $data->user_id : '', + 'account_id' => (int)$data->account_id !== array() ? $data->account_id : '', + 'amount' => (int)$data->amount !== array() ? $data->amount : '', + 'po_number' => $data->po_number !== array() ? $data->po_number : '', + 'terms' => $data->terms !== array() ? $data->terms : '', + 'public_notes' => $data->notes !== array() ? $data->notes : '', + //Best guess on required fields + 'invoice_date' => $data->create_date !== array() ? $data->create_date : '', + 'due_date' => $data->create_date !== array() ? $data->create_date : '', + 'discount' => 0, + 'invoice_footer' => '', + 'invoice_design_id' => 1, + 'invoice_items' => '', + 'is_amount_discount' => 0, + 'partial' => 0, + 'invoice_items' => [ + [ + 'product_key' => '', + 'notes' => $data->notes !== array() ? $data->notes : '', + 'task_public_id' => '', + 'cost' => (int)$data->amount !== array() ? $data->amount : '', + 'qty' => 1, + 'tax' => '', + 'tax_name' => '', + 'tax_rate' => 0 + ] + ], + + ]; + }); + } + + private function arrayToObject($array) + { + $object = new stdClass(); + $object->invoice_number = $array[0]; + $object->organization = $array[1]; + $object->fname = $array[2]; + $object->lname = $array[3]; + $object->amount = $array[4]; + $object->paid = $array[5]; + $object->po_number = $array[6]; + $object->create_date = $array[7]; + $object->date_paid = $array[8]; + $object->terms = $array[9]; + $object->notes = $array[10]; + return $object; + } + + public function validateHeader($csvHeader) + { + $header = [0 => "invoice_number", + 1 => "organization", + 2 => "fname", + 3 => "lname", + 4 => "amount", + 5 => "paid", + 6 => "po_number", + 7 => "create_date", + 8 => "date_paid", + 9 => "terms", + 10 => "notes"]; + + if(!empty(array_diff($header, $csvHeader))) + throw new Exception(trans('texts.invalid_csv_header')); + } + + +} \ No newline at end of file diff --git a/app/Ninja/Import/FreshBooks/StaffTransformer.php b/app/Ninja/Import/FreshBooks/StaffTransformer.php new file mode 100644 index 000000000000..b9dcceef9aae --- /dev/null +++ b/app/Ninja/Import/FreshBooks/StaffTransformer.php @@ -0,0 +1,88 @@ +arrayToObject($data); + return [ + 'account_id' => Auth::user()->account_id, + 'first_name' => $data->fname !== array() ? $data->fname : '', + 'last_name' => $data->lname !== array() ? $data->lname : '', + 'phone' => $data->bus_phone !== array() ? $data->bus_phone : $data->mob_phone, + 'username' => $data->email !== array() ? $data->email : '', + 'email' => $data->email !== array() ? $data->email : '', + ]; + }); + } + + private function arrayToObject($array) + { + $object = new stdClass(); + $object->fname = $array[0]; + $object->lname = $array[1]; + $object->email = $array[2]; + $object->p_stret = $array[3]; + $object->p_street2 = $array[4]; + $object->p_city = $array[5]; + $object->p_province = $array[6]; + $object->p_country = $array[7]; + $object->p_code = $array[8]; + $object->bus_phone = $array[9]; + $object->home_phone = $array[10]; + $object->mob_phone = $array[11]; + $object->fax = $array[12]; + $object->s_street = $array[13]; + $object->s_street2 = $array[14]; + $object->s_city = $array[15]; + $object->s_province = $array[16]; + $object->s_country = $array[17]; + $object->s_code = $array[18]; + return $object; + } + + public function validateHeader($csvHeader) + { + $header = [0 => "fname", + 1 => "lname", + 2 => "email", + 3 => "p_stret", + 4 => "p_street2", + 5 => "p_city", + 6 => "p_province", + 7 => "p_country", + 8 => "p_code", + 9 => "bus_phone", + 10 => "home_phone", + 11 => "mob_phone", + 12 => "fax", + 13 => "s_street", + 14 => "s_street2", + 15 => "s_city", + 16 => "s_province", + 17 => "s_country", + 18 => "s_code"]; + + if(empty($difference)) + return; + + $difference = array_diff($header, $csvHeader); + $difference = implode(',', $difference); + throw new Exception(trans('texts.invalid_csv_header') . " - $difference - "); + } +} \ No newline at end of file diff --git a/app/Ninja/Import/FreshBooks/TimesheetTransformer.php b/app/Ninja/Import/FreshBooks/TimesheetTransformer.php new file mode 100644 index 000000000000..adba19f6946c --- /dev/null +++ b/app/Ninja/Import/FreshBooks/TimesheetTransformer.php @@ -0,0 +1,68 @@ +arrayToObject($data); + // start by converting to seconds + $seconds = ($data->hours * 3600); + $timeLogFinish = strtotime($data->date); + $timeLogStart = intval($timeLogFinish - $seconds); + $timeLog[] = []; + $timelog[] = $timeLogStart; + $timelog[] = $timeLogFinish; + //dd(json_decode("[[$timeLogStart,$timeLogFinish]]")); + $timeLog = json_encode(array($timelog)); + return [ + 'action' => 'stop', + 'time_log' => $timeLog !== array() ? $timeLog : '', + 'user_id' => Auth::user()->id, + 'description' => $data->task !== array() ? $data->task : '', + ]; + }); + } + + private function arrayToObject($array) + { + $object = new stdClass(); + $object->fname = $array[0]; + $object->lname = $array[1]; + $object->date = $array[2]; + $object->project = $array[3]; + $object->task = $array[4]; + $object->hours = $array[5]; + return $object; + } + + public function validateHeader($csvHeader) + { + $header = [ + 0 => "fname", + 1 => "lname", + 2 => "date", + 3 => "project", + 4 => "task", + 5 => "hours"]; + + if(!empty(array_diff($header, $csvHeader))) + throw new Exception(trans('texts.invalid_csv_header')); + } + + +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 72de3e1d2362..8a7e9bfad217 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -189,6 +189,11 @@ class AppServiceProvider extends ServiceProvider { 'Illuminate\Contracts\Auth\Registrar', 'App\Services\Registrar' ); + + $this->app->bind( + 'App\Ninja\Import\DataImporterServiceInterface', + 'App\Ninja\Import\FreshBooks\FreshBooksDataImporterService' + ); } } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 9b61b694134b..90af9d6a304d 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -195,9 +195,6 @@ return array( 'site_updates' => 'Site Updates', 'custom_messages' => 'Custom Messages', 'default_email_footer' => 'Set default email signature', - 'import_clients' => 'Import Client Data', - 'csv_file' => 'Select CSV file', - 'export_clients' => 'Export Client Data', 'select_file' => 'Please select a file', 'first_row_headers' => 'Use first row as headers', 'column' => 'Column', @@ -208,6 +205,18 @@ return array( 'email_settings' => 'Email Settings', 'pdf_email_attachment' => 'Attach PDFs', + //import CSV data pages + 'import_clients' => 'Import Client Data', + 'import_from_freshbooks' => 'Import From FreshBooks', + 'csv_file' => 'Select CSV file', + 'csv_client_file' => 'Select CSV Client file', + 'csv_invoice_file' => 'Select CSV Invoice file', + 'csv_timesheet_file' => 'Select CSV Timesheet file', + 'export_clients' => 'Export Client Data', + 'no_mapper' => 'No valid mapping for file', + 'invalid_csv_header' => 'Invalid CSV Header', + + // application messages 'created_client' => 'Successfully created client', 'created_clients' => 'Successfully created :count clients', @@ -249,6 +258,7 @@ return array( 'archived_credits' => 'Successfully archived :count credits', 'deleted_credit' => 'Successfully deleted credit', 'deleted_credits' => 'Successfully deleted :count credits', + 'imported_file' => 'Successfully imported file', // Emails 'confirmation_subject' => 'Invoice Ninja Account Confirmation', diff --git a/resources/views/accounts/import_export.blade.php b/resources/views/accounts/import_export.blade.php index a621fc4b0e7d..44396e13d1b1 100644 --- a/resources/views/accounts/import_export.blade.php +++ b/resources/views/accounts/import_export.blade.php @@ -17,6 +17,20 @@ {!! Former::close() !!} +{!! Former::open_for_files('settings/' . IMPORT_FROM_FRESHBOOKS) !!} +
+
+

{!! trans('texts.import_from_freshbooks') !!}

+
+
+ {!! Former::file('client_file')->label(trans('texts.csv_client_file')) !!} + {!! Former::file('invoice_file')->label(trans('texts.csv_invoice_file')) !!} + {!! Former::file('timesheet_file')->label(trans('texts.csv_timesheet_file')) !!} + {!! Former::actions( Button::info(trans('texts.upload'))->submit()->large()->appendIcon(Icon::create('open'))) !!} +
+
+{!! Former::close() !!} + {!! Former::open('/export') !!}
diff --git a/resources/views/accounts/import_map.blade.php b/resources/views/accounts/import_map.blade.php index 83921fd22573..9ea33e49c8f4 100644 --- a/resources/views/accounts/import_map.blade.php +++ b/resources/views/accounts/import_map.blade.php @@ -49,7 +49,7 @@ {!! Former::actions( Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/settings/import_export'))->appendIcon(Icon::create('remove-circle')), - Button::success(trans('texts.import'))->submit()->large()->appendIcon(Icon::create('floppy-disk'))) !!} + Button::success(trans('texts.import'))->submit()->large()->appendIcon(Icon::create('floppy-disk'))) !!} {!! Former::close() !!}