Merging import changes

This commit is contained in:
Hillel Coren 2015-11-17 15:53:14 +02:00
parent 9f4cd62832
commit 24bcb0e2e5
12 changed files with 598 additions and 7 deletions

View File

@ -1,6 +1,7 @@
<?php namespace App\Http\Controllers;
use Auth;
use Exception;
use File;
use Image;
use Input;
@ -36,6 +37,7 @@ use App\Models\Timezone;
use App\Models\Industry;
use App\Models\InvoiceDesign;
use App\Models\TaxRate;
use App\Ninja\Import\DataImporterServiceInterface;
use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Repositories\ClientRepository;
use App\Ninja\Repositories\ReferralRepository;
@ -55,7 +57,7 @@ class AccountController extends BaseController
protected $contactMailer;
protected $referralRepository;
public function __construct(AccountRepository $accountRepo, UserMailer $userMailer, ContactMailer $contactMailer, ReferralRepository $referralRepository)
public function __construct(AccountRepository $accountRepo, UserMailer $userMailer, ContactMailer $contactMailer, ReferralRepository $referralRepository, ClientRepository $clientRepository, DataImporterServiceInterface $dataImporterService)
{
parent::__construct();
@ -63,6 +65,8 @@ class AccountController extends BaseController
$this->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');

View File

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

View File

@ -0,0 +1,15 @@
<?php
/**
* Created by PhpStorm.
* User: eduardocruz
* Date: 11/9/15
* Time: 11:03
*/
namespace App\Ninja\Import;
interface DataImporterServiceInterface
{
public function import($files);
}

View File

@ -0,0 +1,107 @@
<?php
/**
* Created by PhpStorm.
* User: eduardocruz
* Date: 11/9/15
* Time: 11:47
*/
namespace app\Ninja\Import\FreshBooks;
use League\Fractal\TransformerAbstract;
use League\Fractal\Resource\Collection;
use stdClass;
use App\Models\Country;
class ClientTransformer extends TransformerAbstract
{
public function transform($data)
{
return new Collection($data, function(array $data) {
$data = $this->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'));
}
}

View File

@ -0,0 +1,154 @@
<?php
/**
* Created by PhpStorm.
* User: eduardocruz
* Date: 11/9/15
* Time: 11:10
*/
namespace App\Ninja\Import\FreshBooks;
use Exception;
use App\Ninja\Import\DataImporterServiceInterface;
use League\Fractal\Manager;
use parseCSV;
use App\Ninja\Repositories\ClientRepository;
use App\Ninja\Repositories\InvoiceRepository;
use Illuminate\Contracts\Container\Container;
class FreshBooksDataImporterService implements DataImporterServiceInterface
{
protected $transformer;
//protected $repository;
protected $invoiceRepo;
/**
* FreshBooksDataImporterService constructor.
*/
public function __construct(Manager $manager, ClientRepository $clientRepo, InvoiceRepository $invoiceRepo, Container $container)
{
$this->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();
}
}

View File

@ -0,0 +1,103 @@
<?php
/**
* Created by PhpStorm.
* User: eduardocruz
* Date: 11/9/15
* Time: 11:47
*/
namespace app\Ninja\Import\FreshBooks;
use App\Models\Client;
use Exception;
use League\Fractal\Resource\Collection;
use League\Fractal\TransformerAbstract;
use stdClass;
class InvoiceTransformer extends TransformerAbstract
{
public function transform($data)
{
return new Collection($data, function(array $data) {
$data = $this->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'));
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* Created by PhpStorm.
* User: eduardocruz
* Date: 11/9/15
* Time: 11:47
*/
namespace app\Ninja\Import\FreshBooks;
use League\Fractal\TransformerAbstract;
use League\Fractal\Resource\Collection;
use stdClass;
use Illuminate\Support\Facades\Auth;
use Exception;
class StaffTransformer extends TransformerAbstract
{
public function transform($data)
{
return new Collection($data, function(array $data) {
$data = $this->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 - ");
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* Created by PhpStorm.
* User: eduardocruz
* Date: 11/9/15
* Time: 11:47
*/
namespace app\Ninja\Import\FreshBooks;
use League\Fractal\TransformerAbstract;
use League\Fractal\Resource\Collection;
use stdClass;
use Illuminate\Support\Facades\Auth;
class TimesheetTransformer extends TransformerAbstract
{
public function transform($data)
{
return new Collection($data, function(array $data) {
$data = $this->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'));
}
}

View File

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

View File

@ -195,9 +195,6 @@ return array(
'site_updates' => 'Site Updates',
'custom_messages' => 'Custom Messages',
'default_email_footer' => 'Set default <b>email signature</b>',
'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',

View File

@ -17,6 +17,20 @@
</div>
{!! Former::close() !!}
{!! Former::open_for_files('settings/' . IMPORT_FROM_FRESHBOOKS) !!}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.import_from_freshbooks') !!}</h3>
</div>
<div class="panel-body">
{!! 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'))) !!}
</div>
</div>
{!! Former::close() !!}
{!! Former::open('/export') !!}
<div class="panel panel-default">

View File

@ -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() !!}
<script type="text/javascript">