Support importing JSON file

This commit is contained in:
Hillel Coren 2016-06-02 22:03:59 +03:00
parent e312f00b80
commit 0acf4ff1fc
23 changed files with 323 additions and 172 deletions

View File

@ -43,10 +43,12 @@ class ExportController extends BaseController
$manager->setSerializer(new ArraySerializer());
$account = Auth::user()->account;
$account->loadAllData();
$account->load(['clients.contacts', 'clients.invoices.payments', 'clients.invoices.invoice_items']);
$resource = new Item($account, new AccountTransformer);
$data = $manager->createData($resource)->toArray();
$data = $manager->parseIncludes('clients.invoices.payments')
->createData($resource)
->toArray();
return response()->json($data);
}

View File

@ -36,8 +36,11 @@ class ImportController extends BaseController
if ($source === IMPORT_CSV) {
$data = $this->importService->mapCSV($files);
return View::make('accounts.import_map', ['data' => $data]);
} elseif ($source === IMPORT_JSON) {
$results = $this->importService->importJSON($files[IMPORT_JSON]);
return $this->showResult($results);
} else {
$results = $this->importService->import($source, $files);
$results = $this->importService->importFiles($source, $files);
return $this->showResult($results);
}
} catch (Exception $exception) {

View File

@ -25,12 +25,12 @@ class CreatePaymentRequest extends PaymentRequest
$invoice = Invoice::scope($input['invoice'])->firstOrFail();
$rules = array(
'client' => 'required',
'invoice' => 'required',
'client' => 'required', // TODO: change to client_id once views are updated
'invoice' => 'required', // TODO: change to invoice_id once views are updated
'amount' => "required|less_than:{$invoice->balance}|positive",
);
if ($input['payment_type_id'] == PAYMENT_TYPE_CREDIT) {
if ( ! empty($input['payment_type_id']) && $input['payment_type_id'] == PAYMENT_TYPE_CREDIT) {
$rules['payment_type_id'] = 'has_credit:'.$input['client'].','.$input['amount'];
}

View File

@ -472,6 +472,7 @@ if (!defined('CONTACT_EMAIL')) {
define('DEFAULT_SEND_RECURRING_HOUR', 8);
define('IMPORT_CSV', 'CSV');
define('IMPORT_JSON', 'JSON');
define('IMPORT_FRESHBOOKS', 'FreshBooks');
define('IMPORT_WAVE', 'Wave');
define('IMPORT_RONIN', 'Ronin');

View File

@ -3,6 +3,7 @@
use Auth;
use Utils;
use App\Models\EntityModel;
use App\Events\ClientWasCreated;
use App\Events\QuoteWasCreated;
use App\Events\InvoiceWasCreated;
@ -63,6 +64,10 @@ class SubscriptionListener
private function checkSubscriptions($eventId, $entity, $transformer, $include = '')
{
if ( ! EntityModel::$notifySubscriptions) {
return;
}
$subscription = $entity->account->getSubscription($eventId);
if ($subscription) {

View File

@ -545,7 +545,7 @@ class Account extends Eloquent
if ($this->hasClientNumberPattern($invoice) && !$clientId) {
// do nothing, we don't yet know the value
} else {
} elseif ( ! $invoice->invoice_number) {
$invoice->invoice_number = $this->getNextInvoiceNumber($invoice);
}
}
@ -649,7 +649,7 @@ class Account extends Eloquent
return $this->getNextInvoiceNumber($invoice);
}
public function getNextInvoiceNumber($invoice)
public function getNextInvoiceNumber($invoice, $validateUnique = true)
{
if ($this->hasNumberPattern($invoice->invoice_type_id)) {
$number = $this->getNumberPattern($invoice);
@ -657,13 +657,16 @@ class Account extends Eloquent
$counter = $this->getCounter($invoice->invoice_type_id);
$prefix = $this->getNumberPrefix($invoice->invoice_type_id);
$counterOffset = 0;
$check = false;
// confirm the invoice number isn't already taken
do {
$number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT);
if ($validateUnique) {
$check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first();
$counter++;
$counterOffset++;
}
} while ($check);
// update the invoice counter to be caught up
@ -688,7 +691,7 @@ class Account extends Eloquent
public function incrementCounter($invoice)
{
// if they didn't use the counter don't increment it
if ($invoice->invoice_number != $this->getNextInvoiceNumber($invoice)) {
if ($invoice->invoice_number != $this->getNextInvoiceNumber($invoice, false)) {
return;
}
@ -1452,6 +1455,14 @@ class Account extends Eloquent
}
}
Account::updated(function ($account) {
Account::updated(function ($account)
{
// prevent firing event if the invoice/quote counter was changed
// TODO: remove once counters are moved to separate table
$dirty = $account->getDirty();
if (isset($dirty['invoice_number_counter']) || isset($dirty['quote_number_counter'])) {
return;
}
Event::fire(new UserSettingsChanged());
});

View File

@ -9,21 +9,30 @@ class EntityModel extends Eloquent
public $timestamps = true;
protected $hidden = ['id'];
public static $notifySubscriptions = true;
public static function createNew($context = null)
{
$className = get_called_class();
$entity = new $className();
if ($context) {
$entity->user_id = $context instanceof User ? $context->id : $context->user_id;
$entity->account_id = $context->account_id;
$user = $context instanceof User ? $context : $context->user;
$account = $context->account;
} elseif (Auth::check()) {
$entity->user_id = Auth::user()->id;
$entity->account_id = Auth::user()->account_id;
$user = Auth::user();
$account = Auth::user()->account;
} else {
Utils::fatalError();
}
$entity->user_id = $user->id;
$entity->account_id = $account->id;
// store references to the original user/account to prevent needing to reload them
$entity->setRelation('user', $user);
$entity->setRelation('account', $account);
if (method_exists($className, 'withTrashed')){
$lastEntity = $className::withTrashed()
->scope(false, $entity->account_id);

View File

@ -5,9 +5,9 @@ use League\Fractal\Resource\Item;
class PaymentTransformer extends BaseTransformer
{
public function transform($data, $maps)
public function transform($data)
{
return new Item($data, function ($data) use ($maps) {
return new Item($data, function ($data) {
return [
'amount' => $data->paid,
'payment_date_sql' => $data->create_date,

View File

@ -5,9 +5,9 @@ use League\Fractal\Resource\Item;
class PaymentTransformer extends BaseTransformer
{
public function transform($data, $maps)
public function transform($data)
{
return new Item($data, function ($data) use ($maps) {
return new Item($data, function ($data) {
return [
'amount' => $data->paid_total,
'payment_date_sql' => $this->getDate($data->last_paid_on),

View File

@ -5,9 +5,9 @@ use League\Fractal\Resource\Item;
class PaymentTransformer extends BaseTransformer
{
public function transform($data, $maps)
public function transform($data)
{
return new Item($data, function ($data) use ($maps) {
return new Item($data, function ($data) {
return [
'amount' => $data->paid,
'payment_date_sql' => $data->date_paid,

View File

@ -5,9 +5,9 @@ use League\Fractal\Resource\Item;
class PaymentTransformer extends BaseTransformer
{
public function transform($data, $maps)
public function transform($data)
{
return new Item($data, function ($data) use ($maps) {
return new Item($data, function ($data) {
return [
'amount' => (float) $data->paid_to_date,
'payment_date_sql' => $this->getDate($data->date),

View File

@ -5,9 +5,9 @@ use League\Fractal\Resource\Item;
class PaymentTransformer extends BaseTransformer
{
public function transform($data, $maps)
public function transform($data)
{
return new Item($data, function ($data) use ($maps) {
return new Item($data, function ($data) {
return [
'amount' => (float) $data->total - (float) $data->balance,
'payment_date_sql' => $data->date_paid,

View File

@ -5,13 +5,13 @@ use League\Fractal\Resource\Item;
class PaymentTransformer extends BaseTransformer
{
public function transform($data, $maps)
public function transform($data)
{
if ( ! $this->getInvoiceClientId($data->invoice_num)) {
return false;
}
return new Item($data, function ($data) use ($maps) {
return new Item($data, function ($data) {
return [
'amount' => (float) $data->amount,
'payment_date_sql' => $this->getDate($data->payment_date),

View File

@ -5,9 +5,9 @@ use League\Fractal\Resource\Item;
class PaymentTransformer extends BaseTransformer
{
public function transform($data, $maps)
public function transform($data)
{
return new Item($data, function ($data) use ($maps) {
return new Item($data, function ($data) {
return [
'amount' => (float) $data->total - (float) $data->balance,
'payment_date_sql' => $data->last_payment_date,

View File

@ -118,11 +118,13 @@ class ClientRepository extends BaseRepository
$first = false;
}
if ( ! $client->wasRecentlyCreated) {
foreach ($client->contacts as $contact) {
if (!in_array($contact->public_id, $contactIds)) {
$contact->delete();
}
}
}
if (!$publicId || $publicId == '-1') {
event(new ClientWasCreated($client));

View File

@ -494,6 +494,7 @@ class InvoiceRepository extends BaseRepository
}
}
if ( ! $invoice->wasRecentlyCreated) {
foreach ($invoice->documents as $document){
if(!in_array($document->public_id, $document_ids)){
// Removed
@ -504,6 +505,7 @@ class InvoiceRepository extends BaseRepository
}
}
}
}
foreach ($data['invoice_items'] as $item) {
$item = (array) $item;

View File

@ -11,7 +11,6 @@ class EventServiceProvider extends ServiceProvider {
* @var array
*/
protected $listen = [
// Clients
'App\Events\ClientWasCreated' => [
'App\Listeners\ActivityListener@createdClient',
@ -151,7 +150,6 @@ class EventServiceProvider extends ServiceProvider {
'App\Events\UserSettingsChanged' => [
'App\Listeners\HandleUserSettingsChanged',
],
];
/**

View File

@ -17,6 +17,7 @@ use App\Ninja\Repositories\ProductRepository;
use App\Ninja\Serializers\ArraySerializer;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\EntityModel;
class ImportService
{
@ -25,9 +26,13 @@ class ImportService
protected $clientRepo;
protected $contactRepo;
protected $productRepo;
protected $processedRows = array();
protected $processedRows = [];
private $maps = [];
public $results = [];
public static $entityTypes = [
IMPORT_JSON,
ENTITY_CLIENT,
ENTITY_CONTACT,
ENTITY_INVOICE,
@ -39,6 +44,7 @@ class ImportService
public static $sources = [
IMPORT_CSV,
IMPORT_JSON,
IMPORT_FRESHBOOKS,
//IMPORT_HARVEST,
IMPORT_HIVEAGE,
@ -68,10 +74,70 @@ class ImportService
$this->productRepo = $productRepo;
}
public function import($source, $files)
public function importJSON($file)
{
$this->init();
$file = file_get_contents($file);
$json = json_decode($file, true);
$json = $this->removeIdFields($json);
$this->checkClientCount(count($json['clients']));
foreach ($json['clients'] as $jsonClient) {
if ($this->validate($jsonClient, ENTITY_CLIENT) === true) {
$client = $this->clientRepo->save($jsonClient);
$this->addSuccess($client);
} else {
$this->addFailure(ENTITY_CLIENT, $jsonClient);
continue;
}
foreach ($jsonClient['invoices'] as $jsonInvoice) {
$jsonInvoice['client_id'] = $client->id;
if ($this->validate($jsonInvoice, ENTITY_INVOICE) === true) {
$invoice = $this->invoiceRepo->save($jsonInvoice);
$this->addSuccess($invoice);
} else {
$this->addFailure(ENTITY_INVOICE, $jsonInvoice);
continue;
}
foreach ($jsonInvoice['payments'] as $jsonPayment) {
$jsonPayment['client_id'] = $jsonPayment['client'] = $client->id; // TODO: change to client_id once views are updated
$jsonPayment['invoice_id'] = $jsonPayment['invoice'] = $invoice->id; // TODO: change to invoice_id once views are updated
if ($this->validate($jsonPayment, ENTITY_PAYMENT) === true) {
$payment = $this->paymentRepo->save($jsonPayment);
$this->addSuccess($payment);
} else {
$this->addFailure(ENTITY_PAYMENT, $jsonPayment);
continue;
}
}
}
}
return $this->results;
}
public function removeIdFields($array)
{
foreach ($array as $key => $val) {
if (is_array($val)) {
$array[$key] = $this->removeIdFields($val);
} elseif ($key === 'id') {
unset($array[$key]);
}
}
return $array;
}
public function importFiles($source, $files)
{
$results = [];
$imported_files = null;
$this->initMaps();
foreach ($files as $entityType => $file) {
$results[$entityType] = $this->execute($source, $entityType, $file);
@ -89,12 +155,12 @@ class ImportService
// Convert the data
$row_list = array();
$maps = $this->createMaps();
Excel::load($file, function ($reader) use ($source, $entityType, $maps, &$row_list, &$results) {
Excel::load($file, function ($reader) use ($source, $entityType, &$row_list, &$results) {
$this->checkData($entityType, count($reader->all()));
$reader->each(function ($row) use ($source, $entityType, $maps, &$row_list, &$results) {
$data_index = $this->transformRow($source, $entityType, $row, $maps);
$reader->each(function ($row) use ($source, $entityType, &$row_list, &$results) {
$data_index = $this->transformRow($source, $entityType, $row);
if ($data_index !== false) {
if ($data_index !== true) {
@ -109,7 +175,7 @@ class ImportService
// Save the data
foreach ($row_list as $row_data) {
$result = $this->saveData($source, $entityType, $row_data['row'], $row_data['data_index'], $maps);
$result = $this->saveData($source, $entityType, $row_data['row'], $row_data['data_index']);
if ($result) {
$results[RESULT_SUCCESS][] = $result;
} else {
@ -120,10 +186,10 @@ class ImportService
return $results;
}
private function transformRow($source, $entityType, $row, $maps)
private function transformRow($source, $entityType, $row)
{
$transformer = $this->getTransformer($source, $entityType, $maps);
$resource = $transformer->transform($row, $maps);
$transformer = $this->getTransformer($source, $entityType, $this->maps);
$resource = $transformer->transform($row);
if (!$resource) {
return false;
@ -138,7 +204,7 @@ class ImportService
$data['invoice_number'] = $account->getNextInvoiceNumber($invoice);
}
if ($this->validate($source, $data, $entityType) !== true) {
if ($this->validate($data, $entityType) !== true) {
return false;
}
@ -160,14 +226,18 @@ class ImportService
return key($this->processedRows);
}
private function saveData($source, $entityType, $row, $data_index, $maps)
private function saveData($source, $entityType, $row, $data_index)
{
$data = $this->processedRows[$data_index];
$entity = $this->{"{$entityType}Repo"}->save($data);
// update the entity maps
$mapFunction = 'add' . ucwords($entity->getEntityType()) . 'ToMaps';
$this->$mapFunction($entity);
// if the invoice is paid we'll also create a payment record
if ($entityType === ENTITY_INVOICE && isset($data['paid']) && $data['paid'] > 0) {
$this->createPayment($source, $row, $maps, $data['client_id'], $entity->id);
$this->createPayment($source, $row, $data['client_id'], $entity->id);
}
return $entity;
@ -200,21 +270,22 @@ class ImportService
return new $className($maps);
}
private function createPayment($source, $data, $maps, $clientId, $invoiceId)
private function createPayment($source, $data, $clientId, $invoiceId)
{
$paymentTransformer = $this->getTransformer($source, ENTITY_PAYMENT, $maps);
$paymentTransformer = $this->getTransformer($source, ENTITY_PAYMENT, $this->maps);
$data->client_id = $clientId;
$data->invoice_id = $invoiceId;
if ($resource = $paymentTransformer->transform($data, $maps)) {
if ($resource = $paymentTransformer->transform($data)) {
$data = $this->fractal->createData($resource)->toArray();
$this->paymentRepo->save($data);
}
}
private function validate($source, $data, $entityType)
private function validate($data, $entityType)
{
/*
// Harvest's contacts are listed separately
if ($entityType === ENTITY_CLIENT && $source != IMPORT_HARVEST) {
$rules = [
@ -234,71 +305,21 @@ class ImportService
'product_key' => 'required',
];
}
*/
$requestClass = 'App\\Http\\Requests\\Create' . ucwords($entityType) . 'Request';
$request = new $requestClass();
$request->setUserResolver(function() { return Auth::user(); });
$request->replace($data);
$validator = Validator::make($data, $rules);
$validator = Validator::make($data, $request->rules());
if ($validator->fails()) {
$messages = $validator->messages();
return $messages->first();
return $validator->messages()->first();
} else {
return true;
}
}
private function createMaps()
{
$clientMap = [];
$clients = $this->clientRepo->all();
foreach ($clients as $client) {
if ($name = strtolower(trim($client->name))) {
$clientMap[$name] = $client->id;
}
}
$invoiceMap = [];
$invoiceClientMap = [];
$invoices = $this->invoiceRepo->all();
foreach ($invoices as $invoice) {
if ($number = strtolower(trim($invoice->invoice_number))) {
$invoiceMap[$number] = $invoice->id;
$invoiceClientMap[$number] = $invoice->client_id;
}
}
$productMap = [];
$products = $this->productRepo->all();
foreach ($products as $product) {
if ($key = strtolower(trim($product->product_key))) {
$productMap[$key] = $product->id;
}
}
$countryMap = [];
$countryMap2 = [];
$countries = Cache::get('countries');
foreach ($countries as $country) {
$countryMap[strtolower($country->name)] = $country->id;
$countryMap2[strtolower($country->iso_3166_2)] = $country->id;
}
$currencyMap = [];
$currencies = Cache::get('currencies');
foreach ($currencies as $currency) {
$currencyMap[strtolower($currency->code)] = $currency->id;
}
return [
ENTITY_CLIENT => $clientMap,
ENTITY_INVOICE => $invoiceMap,
ENTITY_INVOICE.'_'.ENTITY_CLIENT => $invoiceClientMap,
ENTITY_PRODUCT => $productMap,
'countries' => $countryMap,
'countries2' => $countryMap2,
'currencies' => $currencyMap,
];
}
public function mapCSV($files)
{
$data = [];
@ -430,7 +451,7 @@ class ImportService
$data = Session::get("{$entityType}-data");
$this->checkData($entityType, count($data));
$maps = $this->createMaps();
$this->initMaps();
// Convert the data
$row_list = array();
@ -441,7 +462,7 @@ class ImportService
}
$row = $this->convertToObject($entityType, $row, $map);
$data_index = $this->transformRow($source, $entityType, $row, $maps);
$data_index = $this->transformRow($source, $entityType, $row);
if ($data_index !== false) {
if ($data_index !== true) {
@ -455,7 +476,7 @@ class ImportService
// Save the data
foreach ($row_list as $row_data) {
$result = $this->saveData($source, $entityType, $row_data['row'], $row_data['data_index'], $maps);
$result = $this->saveData($source, $entityType, $row_data['row'], $row_data['data_index']);
if ($result) {
$results[RESULT_SUCCESS][] = $result;
@ -493,4 +514,93 @@ class ImportService
return $obj;
}
private function addSuccess($entity)
{
$this->results[$entity->getEntityType()][RESULT_SUCCESS][] = $entity;
}
private function addFailure($entityType, $data)
{
$this->results[$entityType][RESULT_FAILURE][] = $data;
}
private function init()
{
EntityModel::$notifySubscriptions = false;
foreach ([ENTITY_CLIENT, ENTITY_INVOICE, ENTITY_PAYMENT] as $entityType) {
$this->results[$entityType] = [
RESULT_SUCCESS => [],
RESULT_FAILURE => [],
];
}
}
private function initMaps()
{
$this->init();
$this->maps = [
'client' => [],
'invoice' => [],
'invoice_client' => [],
'product' => [],
'countries' => [],
'countries2' => [],
'currencies' => [],
'client_ids' => [],
'invoice_ids' => [],
];
$clients = $this->clientRepo->all();
foreach ($clients as $client) {
$this->addClientToMaps($client);
}
$invoices = $this->invoiceRepo->all();
foreach ($invoices as $invoice) {
$this->addInvoiceToMaps($invoice);
}
$products = $this->productRepo->all();
foreach ($products as $product) {
$this->addProductToMaps($product);
}
$countries = Cache::get('countries');
foreach ($countries as $country) {
$this->maps['countries'][strtolower($country->name)] = $country->id;
$this->maps['countries2'][strtolower($country->iso_3166_2)] = $country->id;
}
$currencies = Cache::get('currencies');
foreach ($currencies as $currency) {
$this->maps['currencies'][strtolower($currency->code)] = $currency->id;
}
}
private function addInvoiceToMaps($invoice)
{
if ($number = strtolower(trim($invoice->invoice_number))) {
$this->maps['invoice'][$number] = $invoice->id;
$this->maps['invoice_client'][$number] = $invoice->client_id;
$this->maps['invoice_ids'][$invoice->public_id] = $invoice->id;
}
}
private function addClientToMaps($client)
{
if ($name = strtolower(trim($client->name))) {
$this->maps['client'][$name] = $client->id;
$this->maps['client_ids'][$client->public_id] = $client->id;
}
}
private function addProductToMaps($product)
{
if ($key = strtolower(trim($product->product_key))) {
$this->maps['product'][$key] = $product->id;
}
}
}

View File

@ -1321,6 +1321,8 @@ $LANG = array(
'products_will_create' => 'products will be created.',
'product_key' => 'Product',
'created_products' => 'Successfully created :count product(s)',
'export_help' => 'Use JSON if you plan to import the data into Invoice Ninja.',
'JSON_file' => 'JSON File',
);

View File

@ -53,7 +53,8 @@
->addOption('CSV', 'CSV')
->addOption('XLS', 'XLS')
->addOption('JSON', 'JSON')
->style('max-width: 200px') !!}
->style('max-width: 200px')
->inlineHelp('export_help') !!}
{!! Former::checkbox('entity_types')
->label('include')
@ -100,6 +101,11 @@
@endif
@endforeach
}
@if ($source === IMPORT_JSON)
if (val === '{{ $source }}') {
$('.JSON-file').show();
}
@endif
@endforeach
}

View File

@ -9,7 +9,7 @@
<link href="{{ asset('favicon-v2.png') }}" rel="shortcut icon" type="image/png">
@endif
<!-- Source: https://github.com/hillelcoren/invoice-ninja -->
<!-- Source: https://github.com/invoiceninja/invoiceninja -->
<!-- Version: {{ NINJA_VERSION }} -->
<meta charset="utf-8">