mirror of
				https://github.com/invoiceninja/invoiceninja.git
				synced 2025-11-04 00:27:32 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			1072 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			1072 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
namespace App\Services;
 | 
						|
 | 
						|
use App\Models\Client;
 | 
						|
use App\Models\Contact;
 | 
						|
use App\Models\EntityModel;
 | 
						|
use App\Models\Expense;
 | 
						|
use App\Models\ExpenseCategory;
 | 
						|
use App\Models\Invoice;
 | 
						|
use App\Models\Payment;
 | 
						|
use App\Models\Product;
 | 
						|
use App\Models\Vendor;
 | 
						|
use App\Models\AccountGatewayToken;
 | 
						|
use App\Ninja\Import\BaseTransformer;
 | 
						|
use App\Ninja\Repositories\ClientRepository;
 | 
						|
use App\Ninja\Repositories\CustomerRepository;
 | 
						|
use App\Ninja\Repositories\ContactRepository;
 | 
						|
use App\Ninja\Repositories\ExpenseCategoryRepository;
 | 
						|
use App\Ninja\Repositories\ExpenseRepository;
 | 
						|
use App\Ninja\Repositories\InvoiceRepository;
 | 
						|
use App\Ninja\Repositories\PaymentRepository;
 | 
						|
use App\Ninja\Repositories\ProductRepository;
 | 
						|
use App\Ninja\Repositories\VendorRepository;
 | 
						|
use App\Ninja\Repositories\TaxRateRepository;
 | 
						|
use App\Ninja\Serializers\ArraySerializer;
 | 
						|
use Auth;
 | 
						|
use Cache;
 | 
						|
use Excel;
 | 
						|
use Exception;
 | 
						|
use File;
 | 
						|
use League\Fractal\Manager;
 | 
						|
use parsecsv;
 | 
						|
use Session;
 | 
						|
use stdClass;
 | 
						|
use Utils;
 | 
						|
use Carbon;
 | 
						|
use League\Csv\Reader;
 | 
						|
use League\Csv\Statement;
 | 
						|
 | 
						|
 | 
						|
/**
 | 
						|
 * Class ImportService.
 | 
						|
 */
 | 
						|
class ImportService
 | 
						|
{
 | 
						|
    /**
 | 
						|
     * @var
 | 
						|
     */
 | 
						|
    protected $transformer;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var InvoiceRepository
 | 
						|
     */
 | 
						|
    protected $invoiceRepo;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var ClientRepository
 | 
						|
     */
 | 
						|
    protected $clientRepo;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var CustomerRepository
 | 
						|
     */
 | 
						|
    protected $customerRepo;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var ContactRepository
 | 
						|
     */
 | 
						|
    protected $contactRepo;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var ProductRepository
 | 
						|
     */
 | 
						|
    protected $productRepo;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var array
 | 
						|
     */
 | 
						|
    protected $processedRows = [];
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var array
 | 
						|
     */
 | 
						|
    private $maps = [];
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var array
 | 
						|
     */
 | 
						|
    public $results = [];
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var array
 | 
						|
     */
 | 
						|
    public static $entityTypes = [
 | 
						|
        IMPORT_JSON,
 | 
						|
        ENTITY_CLIENT,
 | 
						|
        ENTITY_CONTACT,
 | 
						|
        ENTITY_INVOICE,
 | 
						|
        ENTITY_PAYMENT,
 | 
						|
        ENTITY_TASK,
 | 
						|
        ENTITY_PRODUCT,
 | 
						|
        ENTITY_VENDOR,
 | 
						|
        ENTITY_EXPENSE,
 | 
						|
        ENTITY_CUSTOMER,
 | 
						|
    ];
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var array
 | 
						|
     */
 | 
						|
    public static $sources = [
 | 
						|
        IMPORT_CSV,
 | 
						|
        IMPORT_JSON,
 | 
						|
        IMPORT_FRESHBOOKS,
 | 
						|
        IMPORT_HIVEAGE,
 | 
						|
        IMPORT_INVOICEABLE,
 | 
						|
        IMPORT_INVOICEPLANE,
 | 
						|
        IMPORT_NUTCACHE,
 | 
						|
        IMPORT_PANCAKE,
 | 
						|
        IMPORT_RONIN,
 | 
						|
        IMPORT_STRIPE,
 | 
						|
        IMPORT_WAVE,
 | 
						|
        IMPORT_ZOHO,
 | 
						|
    ];
 | 
						|
 | 
						|
    /**
 | 
						|
     * ImportService constructor.
 | 
						|
     *
 | 
						|
     * @param Manager           $manager
 | 
						|
     * @param ClientRepository  $clientRepo
 | 
						|
     * @param CustomerRepository $customerRepo
 | 
						|
     * @param InvoiceRepository $invoiceRepo
 | 
						|
     * @param PaymentRepository $paymentRepo
 | 
						|
     * @param ContactRepository $contactRepo
 | 
						|
     * @param ProductRepository $productRepo
 | 
						|
     */
 | 
						|
    public function __construct(
 | 
						|
        Manager $manager,
 | 
						|
        ClientRepository $clientRepo,
 | 
						|
        CustomerRepository $customerRepo,
 | 
						|
        InvoiceRepository $invoiceRepo,
 | 
						|
        PaymentRepository $paymentRepo,
 | 
						|
        ContactRepository $contactRepo,
 | 
						|
        ProductRepository $productRepo,
 | 
						|
        ExpenseRepository $expenseRepo,
 | 
						|
        VendorRepository $vendorRepo,
 | 
						|
        ExpenseCategoryRepository $expenseCategoryRepo,
 | 
						|
        TaxRateRepository $taxRateRepository
 | 
						|
    ) {
 | 
						|
        $this->fractal = $manager;
 | 
						|
        $this->fractal->setSerializer(new ArraySerializer());
 | 
						|
 | 
						|
        $this->clientRepo = $clientRepo;
 | 
						|
        $this->customerRepo = $customerRepo;
 | 
						|
        $this->invoiceRepo = $invoiceRepo;
 | 
						|
        $this->paymentRepo = $paymentRepo;
 | 
						|
        $this->contactRepo = $contactRepo;
 | 
						|
        $this->productRepo = $productRepo;
 | 
						|
        $this->expenseRepo = $expenseRepo;
 | 
						|
        $this->vendorRepo = $vendorRepo;
 | 
						|
        $this->expenseCategoryRepo = $expenseCategoryRepo;
 | 
						|
        $this->taxRateRepository = $taxRateRepository;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $file
 | 
						|
     *
 | 
						|
     * @throws Exception
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function importJSON($fileName, $includeData, $includeSettings)
 | 
						|
    {
 | 
						|
        $this->initMaps();
 | 
						|
        $this->checkForFile($fileName);
 | 
						|
        $file = file_get_contents($fileName);
 | 
						|
        $json = json_decode($file, true);
 | 
						|
        $json = $this->removeIdFields($json);
 | 
						|
        $transformer = new BaseTransformer($this->maps);
 | 
						|
 | 
						|
        $this->checkClientCount(count($json['clients']));
 | 
						|
 | 
						|
        if ($includeSettings) {
 | 
						|
            // remove blank id values
 | 
						|
            $settings = [];
 | 
						|
            foreach ($json as $field => $value) {
 | 
						|
                if (strstr($field, '_id') && ! $value) {
 | 
						|
                    // continue;
 | 
						|
                } else {
 | 
						|
                    $settings[$field] = $value;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            $account = Auth::user()->account;
 | 
						|
            $account->fill($settings);
 | 
						|
            $account->save();
 | 
						|
 | 
						|
            $emailSettings = $account->account_email_settings;
 | 
						|
            $emailSettings->fill(isset($settings['account_email_settings']) ? $settings['account_email_settings'] : $settings);
 | 
						|
            $emailSettings->save();
 | 
						|
        }
 | 
						|
 | 
						|
        if ($includeData) {
 | 
						|
            foreach ($json['products'] as $jsonProduct) {
 | 
						|
                if ($transformer->hasProduct($jsonProduct['product_key'])) {
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
 | 
						|
                $productValidate = EntityModel::validate($jsonProduct, ENTITY_PRODUCT);
 | 
						|
                if ($productValidate === true) {
 | 
						|
                    $product = $this->productRepo->save($jsonProduct);
 | 
						|
                    $this->addProductToMaps($product);
 | 
						|
                    $this->addSuccess($product);
 | 
						|
                } else {
 | 
						|
                    $jsonProduct['type'] = ENTITY_PRODUCT;
 | 
						|
                    $jsonProduct['error'] = $productValidate;
 | 
						|
                    $this->addFailure(ENTITY_PRODUCT, $jsonProduct);
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            foreach ($json['clients'] as $jsonClient) {
 | 
						|
                $clientValidate = EntityModel::validate($jsonClient, ENTITY_CLIENT);
 | 
						|
                if ($clientValidate === true) {
 | 
						|
                    $client = $this->clientRepo->save($jsonClient);
 | 
						|
                    $this->addClientToMaps($client);
 | 
						|
                    $this->addSuccess($client);
 | 
						|
                } else {
 | 
						|
                    $jsonClient['type'] = ENTITY_CLIENT;
 | 
						|
                    $jsonClient['error'] = $clientValidate;
 | 
						|
                    $this->addFailure(ENTITY_CLIENT, $jsonClient);
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
 | 
						|
                foreach ($jsonClient['invoices'] as $jsonInvoice) {
 | 
						|
                    $jsonInvoice['client_id'] = $client->id;
 | 
						|
                    $invoiceValidate = EntityModel::validate($jsonInvoice, ENTITY_INVOICE);
 | 
						|
                    if ($invoiceValidate === true) {
 | 
						|
                        $invoice = $this->invoiceRepo->save($jsonInvoice);
 | 
						|
                        $this->addInvoiceToMaps($invoice);
 | 
						|
                        $this->addSuccess($invoice);
 | 
						|
                    } else {
 | 
						|
                        $jsonInvoice['type'] = ENTITY_INVOICE;
 | 
						|
                        $jsonInvoice['error'] = $invoiceValidate;
 | 
						|
                        $this->addFailure(ENTITY_INVOICE, $jsonInvoice);
 | 
						|
                        continue;
 | 
						|
                    }
 | 
						|
 | 
						|
                    foreach ($jsonInvoice['payments'] as $jsonPayment) {
 | 
						|
                        $jsonPayment['invoice_id'] = $invoice->public_id;
 | 
						|
                        $paymentValidate = EntityModel::validate($jsonPayment, ENTITY_PAYMENT);
 | 
						|
                        if ($paymentValidate === true) {
 | 
						|
                            $jsonPayment['client_id'] = $client->id;
 | 
						|
                            $jsonPayment['invoice_id'] = $invoice->id;
 | 
						|
                            $payment = $this->paymentRepo->save($jsonPayment);
 | 
						|
                            $this->addSuccess($payment);
 | 
						|
                        } else {
 | 
						|
                            $jsonPayment['type'] = ENTITY_PAYMENT;
 | 
						|
                            $jsonPayment['error'] = $paymentValidate;
 | 
						|
                            $this->addFailure(ENTITY_PAYMENT, $jsonPayment);
 | 
						|
                            continue;
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        File::delete($fileName);
 | 
						|
 | 
						|
        return $this->results;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $array
 | 
						|
     *
 | 
						|
     * @return mixed
 | 
						|
     */
 | 
						|
    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;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $source
 | 
						|
     * @param $files
 | 
						|
     *
 | 
						|
     * @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);
 | 
						|
        }
 | 
						|
 | 
						|
        return $results;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $source
 | 
						|
     * @param $entityType
 | 
						|
     * @param $file
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    private function execute($source, $entityType, $fileName)
 | 
						|
    {
 | 
						|
        $results = [
 | 
						|
            RESULT_SUCCESS => [],
 | 
						|
            RESULT_FAILURE => [],
 | 
						|
        ];
 | 
						|
 | 
						|
        // Convert the data
 | 
						|
        $row_list = [];
 | 
						|
        $this->checkForFile($fileName);
 | 
						|
 | 
						|
        Excel::load($fileName, function ($reader) use ($source, $entityType, &$row_list, &$results) {
 | 
						|
            $this->checkData($entityType, count($reader->all()));
 | 
						|
 | 
						|
            $reader->each(function ($row) use ($source, $entityType, &$row_list, &$results) {
 | 
						|
                if ($this->isRowEmpty($row)) {
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
 | 
						|
                $data_index = $this->transformRow($source, $entityType, $row);
 | 
						|
 | 
						|
                if ($data_index !== false) {
 | 
						|
                    if ($data_index !== true) {
 | 
						|
                        // Wasn't merged with another row
 | 
						|
                        $row_list[] = ['row' => $row, 'data_index' => $data_index];
 | 
						|
                    }
 | 
						|
                } else {
 | 
						|
                    $results[RESULT_FAILURE][] = $row;
 | 
						|
                }
 | 
						|
            });
 | 
						|
        });
 | 
						|
 | 
						|
        // Save the data
 | 
						|
        foreach ($row_list as $row_data) {
 | 
						|
            $result = $this->saveData($source, $entityType, $row_data['row'], $row_data['data_index']);
 | 
						|
            if ($result) {
 | 
						|
                $results[RESULT_SUCCESS][] = $result;
 | 
						|
            } else {
 | 
						|
                $results[RESULT_FAILURE][] = $row_data['row'];
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        File::delete($fileName);
 | 
						|
 | 
						|
        return $results;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $source
 | 
						|
     * @param $entityType
 | 
						|
     * @param $row
 | 
						|
     *
 | 
						|
     * @return bool|mixed
 | 
						|
     */
 | 
						|
    private function transformRow($source, $entityType, $row)
 | 
						|
    {
 | 
						|
        $transformer = $this->getTransformer($source, $entityType, $this->maps);
 | 
						|
        $resource = $transformer->transform($row);
 | 
						|
 | 
						|
        if (! $resource) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        $data = $this->fractal->createData($resource)->toArray();
 | 
						|
 | 
						|
        // Create expesnse category
 | 
						|
        if ($entityType == ENTITY_EXPENSE) {
 | 
						|
            if (! empty($row->expense_category)) {
 | 
						|
                $categoryId = $transformer->getExpenseCategoryId($row->expense_category);
 | 
						|
                if (! $categoryId) {
 | 
						|
                    $category = $this->expenseCategoryRepo->save(['name' => $row->expense_category]);
 | 
						|
                    $this->addExpenseCategoryToMaps($category);
 | 
						|
                    $data['expense_category_id'] = $category->id;
 | 
						|
                }
 | 
						|
            }
 | 
						|
            if (! empty($row->vendor) && ($vendorName = trim($row->vendor))) {
 | 
						|
                if (! $transformer->getVendorId($vendorName)) {
 | 
						|
                    $vendor = $this->vendorRepo->save(['name' => $vendorName, 'vendor_contact' => []]);
 | 
						|
                    $this->addVendorToMaps($vendor);
 | 
						|
                    $data['vendor_id'] = $vendor->id;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        /*
 | 
						|
        // if the invoice number is blank we'll assign it
 | 
						|
        if ($entityType == ENTITY_INVOICE && ! $data['invoice_number']) {
 | 
						|
            $account = Auth::user()->account;
 | 
						|
            $invoice = Invoice::createNew();
 | 
						|
            $data['invoice_number'] = $account->getNextNumber($invoice);
 | 
						|
        }
 | 
						|
        */
 | 
						|
 | 
						|
        if (EntityModel::validate($data, $entityType) !== true) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if ($entityType == ENTITY_INVOICE) {
 | 
						|
            if (empty($this->processedRows[$data['invoice_number']])) {
 | 
						|
                $this->processedRows[$data['invoice_number']] = $data;
 | 
						|
            } else {
 | 
						|
                // Merge invoice items
 | 
						|
                $this->processedRows[$data['invoice_number']]['invoice_items'] = array_merge($this->processedRows[$data['invoice_number']]['invoice_items'], $data['invoice_items']);
 | 
						|
 | 
						|
                return true;
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            $this->processedRows[] = $data;
 | 
						|
        }
 | 
						|
 | 
						|
        end($this->processedRows);
 | 
						|
 | 
						|
        return key($this->processedRows);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $source
 | 
						|
     * @param $entityType
 | 
						|
     * @param $row
 | 
						|
     * @param $data_index
 | 
						|
     *
 | 
						|
     * @return mixed
 | 
						|
     */
 | 
						|
    private function saveData($source, $entityType, $row, $data_index)
 | 
						|
    {
 | 
						|
        $data = $this->processedRows[$data_index];
 | 
						|
 | 
						|
        if ($entityType == ENTITY_INVOICE) {
 | 
						|
            $data['is_public'] = true;
 | 
						|
        }
 | 
						|
 | 
						|
        $entity = $this->{"{$entityType}Repo"}->save($data);
 | 
						|
 | 
						|
        // update the entity maps
 | 
						|
        if ($entityType != ENTITY_CUSTOMER) {
 | 
						|
            $mapFunction = 'add' . ucwords($entity->getEntityType()) . 'ToMaps';
 | 
						|
            if (method_exists($this, $mapFunction)) {
 | 
						|
                $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, $data['client_id'], $entity->id, $entity->public_id);
 | 
						|
        }
 | 
						|
 | 
						|
        return $entity;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $entityType
 | 
						|
     * @param $count
 | 
						|
     *
 | 
						|
     * @throws Exception
 | 
						|
     */
 | 
						|
    private function checkData($entityType, $count)
 | 
						|
    {
 | 
						|
        if (Utils::isNinja() && $count > MAX_IMPORT_ROWS) {
 | 
						|
            throw new Exception(trans('texts.limit_import_rows', ['count' => MAX_IMPORT_ROWS]));
 | 
						|
        }
 | 
						|
 | 
						|
        if ($entityType === ENTITY_CLIENT) {
 | 
						|
            $this->checkClientCount($count);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $count
 | 
						|
     *
 | 
						|
     * @throws Exception
 | 
						|
     */
 | 
						|
    private function checkClientCount($count)
 | 
						|
    {
 | 
						|
        $totalClients = $count + Client::scope()->withTrashed()->count();
 | 
						|
        if ($totalClients > Auth::user()->getMaxNumClients()) {
 | 
						|
            throw new Exception(trans('texts.limit_clients', ['count' => Auth::user()->getMaxNumClients()]));
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $source
 | 
						|
     * @param $entityType
 | 
						|
     *
 | 
						|
     * @return string
 | 
						|
     */
 | 
						|
    public static function getTransformerClassName($source, $entityType)
 | 
						|
    {
 | 
						|
        return 'App\\Ninja\\Import\\'.$source.'\\'.ucwords($entityType).'Transformer';
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $source
 | 
						|
     * @param $entityType
 | 
						|
     * @param $maps
 | 
						|
     *
 | 
						|
     * @return mixed
 | 
						|
     */
 | 
						|
    public static function getTransformer($source, $entityType, $maps)
 | 
						|
    {
 | 
						|
        $className = self::getTransformerClassName($source, $entityType);
 | 
						|
 | 
						|
        return new $className($maps);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $source
 | 
						|
     * @param $data
 | 
						|
     * @param $clientId
 | 
						|
     * @param $invoiceId
 | 
						|
     */
 | 
						|
    private function createPayment($source, $row, $clientId, $invoiceId, $invoicePublicId)
 | 
						|
    {
 | 
						|
        $paymentTransformer = $this->getTransformer($source, ENTITY_PAYMENT, $this->maps);
 | 
						|
 | 
						|
        $row->client_id = $clientId;
 | 
						|
        $row->invoice_id = $invoiceId;
 | 
						|
 | 
						|
        if ($resource = $paymentTransformer->transform($row)) {
 | 
						|
            $data = $this->fractal->createData($resource)->toArray();
 | 
						|
            $data['invoice_id'] = $invoicePublicId;
 | 
						|
            if (Payment::validate($data) === true) {
 | 
						|
                $data['invoice_id'] = $invoiceId;
 | 
						|
                $this->paymentRepo->save($data);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array $files
 | 
						|
     *
 | 
						|
     * @throws Exception
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function mapCSV(array $files)
 | 
						|
    {
 | 
						|
        $data = [];
 | 
						|
 | 
						|
        foreach ($files as $entityType => $filename) {
 | 
						|
            $class = 'App\\Models\\' . ucwords($entityType);
 | 
						|
            $columns = $class::getImportColumns();
 | 
						|
            $map = $class::getImportMap();
 | 
						|
 | 
						|
            // Lookup field translations
 | 
						|
            foreach ($columns as $key => $value) {
 | 
						|
                unset($columns[$key]);
 | 
						|
                $label = $value;
 | 
						|
                // disambiguate some of the labels
 | 
						|
                if ($entityType == ENTITY_INVOICE) {
 | 
						|
                    if ($label == 'name') {
 | 
						|
                        $label = 'client_name';
 | 
						|
                    } elseif ($label == 'notes') {
 | 
						|
                        $label = 'product_notes';
 | 
						|
                    } elseif ($label == 'terms') {
 | 
						|
                        $label = 'invoice_terms';
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                $columns[$value] = trans("texts.{$label}");
 | 
						|
            }
 | 
						|
            array_unshift($columns, ' ');
 | 
						|
 | 
						|
            $data[$entityType] = $this->mapFile($entityType, $filename, $columns, $map);
 | 
						|
 | 
						|
            if ($entityType === ENTITY_CLIENT) {
 | 
						|
                if (count($data[$entityType]['data']) + Client::scope()->count() > Auth::user()->getMaxNumClients()) {
 | 
						|
                    throw new Exception(trans('texts.limit_clients', ['count' => Auth::user()->getMaxNumClients()]));
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $data;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $entityType
 | 
						|
     * @param $filename
 | 
						|
     * @param $columns
 | 
						|
     * @param $map
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function mapFile($entityType, $filename, $columns, $map)
 | 
						|
    {
 | 
						|
        $data = $this->getCsvData($filename);
 | 
						|
        $headers = false;
 | 
						|
        $hasHeaders = false;
 | 
						|
        $mapped = [];
 | 
						|
 | 
						|
        if (count($data) > 0) {
 | 
						|
            $headers = $data[0];
 | 
						|
            foreach ($headers as $title) {
 | 
						|
                if (strpos(strtolower($title), 'name') > 0) {
 | 
						|
                    $hasHeaders = true;
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            for ($i = 0; $i < count($headers); $i++) {
 | 
						|
                $title = strtolower($headers[$i]);
 | 
						|
                $mapped[$i] = '';
 | 
						|
 | 
						|
                foreach ($map as $search => $column) {
 | 
						|
                    if ($this->checkForMatch($title, $search)) {
 | 
						|
                        $hasHeaders = true;
 | 
						|
                        $mapped[$i] = $column;
 | 
						|
                        break;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        $data = [
 | 
						|
            'entityType' => $entityType,
 | 
						|
            'data' => $data,
 | 
						|
            'headers' => $headers,
 | 
						|
            'hasHeaders' => $hasHeaders,
 | 
						|
            'columns' => $columns,
 | 
						|
            'mapped' => $mapped,
 | 
						|
            'warning' => false,
 | 
						|
        ];
 | 
						|
 | 
						|
        // check that dates are valid
 | 
						|
        if (count($data['data']) > 1) {
 | 
						|
            $row = $data['data'][1];
 | 
						|
            foreach ($mapped as $index => $field) {
 | 
						|
                if (! strstr($field, 'date')) {
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
                try {
 | 
						|
                    $date = new Carbon($row[$index]);
 | 
						|
                } catch(Exception $e) {
 | 
						|
                    $data['warning'] = 'invalid_date';
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $data;
 | 
						|
    }
 | 
						|
 | 
						|
    private function getCsvData($fileName)
 | 
						|
    {
 | 
						|
        $this->checkForFile($fileName);
 | 
						|
 | 
						|
        if (! ini_get('auto_detect_line_endings')) {
 | 
						|
            ini_set('auto_detect_line_endings', '1');
 | 
						|
        }
 | 
						|
 | 
						|
        $csv = Reader::createFromPath($fileName, 'r');
 | 
						|
        //$csv->setHeaderOffset(0); //set the CSV header offset
 | 
						|
        $stmt = new Statement();
 | 
						|
        $data = iterator_to_array($stmt->process($csv));
 | 
						|
 | 
						|
        if (count($data) > 0) {
 | 
						|
            $headers = $data[0];
 | 
						|
 | 
						|
            // Remove Invoice Ninja headers
 | 
						|
            if (count($headers) && count($data) > 4) {
 | 
						|
                $firstCell = $headers[0];
 | 
						|
                if (strstr($firstCell, APP_NAME)) {
 | 
						|
                    array_shift($data); // Invoice Ninja...
 | 
						|
                    array_shift($data); // <blank line>
 | 
						|
                    array_shift($data); // Enitty Type Header
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $data;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $column
 | 
						|
     * @param $pattern
 | 
						|
     *
 | 
						|
     * @return bool
 | 
						|
     */
 | 
						|
    private function checkForMatch($column, $pattern)
 | 
						|
    {
 | 
						|
        if (strpos($column, 'sec') === 0) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if (strpos($pattern, '^')) {
 | 
						|
            list($include, $exclude) = explode('^', $pattern);
 | 
						|
            $includes = explode('|', $include);
 | 
						|
            $excludes = explode('|', $exclude);
 | 
						|
        } else {
 | 
						|
            $includes = explode('|', $pattern);
 | 
						|
            $excludes = [];
 | 
						|
        }
 | 
						|
 | 
						|
        foreach ($includes as $string) {
 | 
						|
            if (strpos($column, $string) !== false) {
 | 
						|
                $excluded = false;
 | 
						|
                foreach ($excludes as $exclude) {
 | 
						|
                    if (strpos($column, $exclude) !== false) {
 | 
						|
                        $excluded = true;
 | 
						|
                        break;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                if (! $excluded) {
 | 
						|
                    return true;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param array $maps
 | 
						|
     * @param $headers
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function importCSV(array $maps, $headers, $timestamp)
 | 
						|
    {
 | 
						|
        $results = [];
 | 
						|
 | 
						|
        foreach ($maps as $entityType => $map) {
 | 
						|
            $results[$entityType] = $this->executeCSV($entityType, $map, $headers[$entityType], $timestamp);
 | 
						|
        }
 | 
						|
 | 
						|
        return $results;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $entityType
 | 
						|
     * @param $map
 | 
						|
     * @param $hasHeaders
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    private function executeCSV($entityType, $map, $hasHeaders, $timestamp)
 | 
						|
    {
 | 
						|
        $results = [
 | 
						|
            RESULT_SUCCESS => [],
 | 
						|
            RESULT_FAILURE => [],
 | 
						|
        ];
 | 
						|
        $source = IMPORT_CSV;
 | 
						|
 | 
						|
        $path = env('FILE_IMPORT_PATH') ?: storage_path() . '/import';
 | 
						|
        $fileName = sprintf('%s/%s_%s_%s.csv', $path, Auth::user()->account_id, $timestamp, $entityType);
 | 
						|
        $data = $this->getCsvData($fileName);
 | 
						|
        $this->checkData($entityType, count($data));
 | 
						|
        $this->initMaps();
 | 
						|
 | 
						|
        // Convert the data
 | 
						|
        $row_list = [];
 | 
						|
        foreach ($data as $row) {
 | 
						|
            if ($hasHeaders) {
 | 
						|
                $hasHeaders = false;
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            $row = $this->convertToObject($entityType, $row, $map);
 | 
						|
            if ($this->isRowEmpty($row)) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
            $data_index = $this->transformRow($source, $entityType, $row);
 | 
						|
 | 
						|
            if ($data_index !== false) {
 | 
						|
                if ($data_index !== true) {
 | 
						|
                    // Wasn't merged with another row
 | 
						|
                    $row_list[] = ['row' => $row, 'data_index' => $data_index];
 | 
						|
                }
 | 
						|
            } else {
 | 
						|
                $results[RESULT_FAILURE][] = $row;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // Save the data
 | 
						|
        foreach ($row_list as $row_data) {
 | 
						|
            $result = $this->saveData($source, $entityType, $row_data['row'], $row_data['data_index']);
 | 
						|
 | 
						|
            if ($result) {
 | 
						|
                $results[RESULT_SUCCESS][] = $result;
 | 
						|
            } else {
 | 
						|
                $results[RESULT_FAILURE][] = $row;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        File::delete($fileName);
 | 
						|
 | 
						|
        return $results;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $entityType
 | 
						|
     * @param $data
 | 
						|
     * @param $map
 | 
						|
     *
 | 
						|
     * @return stdClass
 | 
						|
     */
 | 
						|
    private function convertToObject($entityType, $data, $map)
 | 
						|
    {
 | 
						|
        $obj = new stdClass();
 | 
						|
        $class = 'App\\Models\\' . ucwords($entityType);
 | 
						|
        $columns = $class::getImportColumns();
 | 
						|
 | 
						|
        foreach ($columns as $column) {
 | 
						|
            $obj->$column = false;
 | 
						|
        }
 | 
						|
 | 
						|
        foreach ($map as $index => $field) {
 | 
						|
            if (! $field) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            if (isset($obj->$field) && $obj->$field) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            if (isset($data[$index])) {
 | 
						|
                $obj->$field = $data[$index];
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $obj;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $entity
 | 
						|
     */
 | 
						|
    private function addSuccess($entity)
 | 
						|
    {
 | 
						|
        $this->results[$entity->getEntityType()][RESULT_SUCCESS][] = $entity;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param $entityType
 | 
						|
     * @param $data
 | 
						|
     */
 | 
						|
    private function addFailure($entityType, $data)
 | 
						|
    {
 | 
						|
        $this->results[$entityType][RESULT_FAILURE][] = $data;
 | 
						|
    }
 | 
						|
 | 
						|
    private function init()
 | 
						|
    {
 | 
						|
        EntityModel::$notifySubscriptions = false;
 | 
						|
 | 
						|
        foreach ([ENTITY_CLIENT, ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_QUOTE, ENTITY_PRODUCT] as $entityType) {
 | 
						|
            $this->results[$entityType] = [
 | 
						|
                RESULT_SUCCESS => [],
 | 
						|
                RESULT_FAILURE => [],
 | 
						|
            ];
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private function initMaps()
 | 
						|
    {
 | 
						|
        $this->init();
 | 
						|
 | 
						|
        $this->maps = [
 | 
						|
            'client' => [],
 | 
						|
            'contact' => [],
 | 
						|
            'customer' => [],
 | 
						|
            'invoice' => [],
 | 
						|
            'invoice_client' => [],
 | 
						|
            'product' => [],
 | 
						|
            'countries' => [],
 | 
						|
            'countries2' => [],
 | 
						|
            'currencies' => [],
 | 
						|
            'client_ids' => [],
 | 
						|
            'invoice_ids' => [],
 | 
						|
            'vendors' => [],
 | 
						|
            'expense_categories' => [],
 | 
						|
            'tax_rates' => [],
 | 
						|
            'tax_names' => [],
 | 
						|
        ];
 | 
						|
 | 
						|
        $clients = $this->clientRepo->all();
 | 
						|
        foreach ($clients as $client) {
 | 
						|
            $this->addClientToMaps($client);
 | 
						|
        }
 | 
						|
 | 
						|
        $customers = $this->customerRepo->all();
 | 
						|
        foreach ($customers as $customer) {
 | 
						|
            $this->addCustomerToMaps($customer);
 | 
						|
        }
 | 
						|
 | 
						|
        $contacts = $this->contactRepo->all();
 | 
						|
        foreach ($contacts as $contact) {
 | 
						|
            $this->addContactToMaps($contact);
 | 
						|
        }
 | 
						|
 | 
						|
        $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;
 | 
						|
        }
 | 
						|
 | 
						|
        $vendors = $this->vendorRepo->all();
 | 
						|
        foreach ($vendors as $vendor) {
 | 
						|
            $this->addVendorToMaps($vendor);
 | 
						|
        }
 | 
						|
 | 
						|
        $expenseCaegories = $this->expenseCategoryRepo->all();
 | 
						|
        foreach ($expenseCaegories as $category) {
 | 
						|
            $this->addExpenseCategoryToMaps($category);
 | 
						|
        }
 | 
						|
 | 
						|
        $taxRates = $this->taxRateRepository->all();
 | 
						|
        foreach ($taxRates as $taxRate) {
 | 
						|
            $name = trim(strtolower($taxRate->name));
 | 
						|
            $this->maps['tax_rates'][$name] = $taxRate->rate;
 | 
						|
            $this->maps['tax_names'][$name] = $taxRate->name;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param Invoice $invoice
 | 
						|
     */
 | 
						|
    private function addInvoiceToMaps(Invoice $invoice)
 | 
						|
    {
 | 
						|
        if ($number = strtolower(trim($invoice->invoice_number))) {
 | 
						|
            $this->maps['invoices'][$number] = $invoice;
 | 
						|
            $this->maps['invoice'][$number] = $invoice->id;
 | 
						|
            $this->maps['invoice_client'][$number] = $invoice->client_id;
 | 
						|
            $this->maps['invoice_ids'][$invoice->public_id] = $invoice->id;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param Client $client
 | 
						|
     */
 | 
						|
    private function addClientToMaps(Client $client)
 | 
						|
    {
 | 
						|
        if ($name = strtolower(trim($client->name))) {
 | 
						|
            $this->maps['client'][$name] = $client->id;
 | 
						|
            $this->maps['client_ids'][$client->public_id] = $client->id;
 | 
						|
        }
 | 
						|
        if ($client->contacts->count() && $name = strtolower(trim($client->contacts[0]->email))) {
 | 
						|
            $this->maps['client'][$name] = $client->id;
 | 
						|
            $this->maps['client_ids'][$client->public_id] = $client->id;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param Customer $customer
 | 
						|
     */
 | 
						|
    private function addCustomerToMaps(AccountGatewayToken $customer)
 | 
						|
    {
 | 
						|
        $this->maps['customer'][$customer->token] = $customer;
 | 
						|
        $this->maps['customer'][$customer->contact->email] = $customer;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param Product $product
 | 
						|
     */
 | 
						|
    private function addContactToMaps(Contact $contact)
 | 
						|
    {
 | 
						|
        if ($key = strtolower(trim($contact->email))) {
 | 
						|
            $this->maps['contact'][$key] = $contact;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param Product $product
 | 
						|
     */
 | 
						|
    private function addProductToMaps(Product $product)
 | 
						|
    {
 | 
						|
        if ($key = strtolower(trim($product->product_key))) {
 | 
						|
            $this->maps['product'][$key] = $product;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private function addExpenseToMaps(Expense $expense)
 | 
						|
    {
 | 
						|
        // do nothing
 | 
						|
    }
 | 
						|
 | 
						|
    private function addVendorToMaps(Vendor $vendor)
 | 
						|
    {
 | 
						|
        $this->maps['vendor'][strtolower($vendor->name)] = $vendor->id;
 | 
						|
    }
 | 
						|
 | 
						|
    private function addExpenseCategoryToMaps(ExpenseCategory $category)
 | 
						|
    {
 | 
						|
        if ($name = strtolower($category->name)) {
 | 
						|
            $this->maps['expense_category'][$name] = $category->id;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private function isRowEmpty($row)
 | 
						|
    {
 | 
						|
        $isEmpty = true;
 | 
						|
 | 
						|
        foreach ($row as $key => $val) {
 | 
						|
            if (trim($val)) {
 | 
						|
                $isEmpty = false;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $isEmpty;
 | 
						|
    }
 | 
						|
 | 
						|
    public function presentResults($results, $includeSettings = false)
 | 
						|
    {
 | 
						|
        $message = '';
 | 
						|
        $skipped = [];
 | 
						|
 | 
						|
        if ($includeSettings) {
 | 
						|
            $message = trans('texts.imported_settings') . '<br/>';
 | 
						|
        }
 | 
						|
 | 
						|
        foreach ($results as $entityType => $entityResults) {
 | 
						|
            if ($count = count($entityResults[RESULT_SUCCESS])) {
 | 
						|
                $message .= trans("texts.created_{$entityType}s", ['count' => $count]) . '<br/>';
 | 
						|
            }
 | 
						|
            if (count($entityResults[RESULT_FAILURE])) {
 | 
						|
                $skipped = array_merge($skipped, $entityResults[RESULT_FAILURE]);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (count($skipped)) {
 | 
						|
            $message .= '<p/>' . trans('texts.failed_to_import') . '<br/>';
 | 
						|
            foreach ($skipped as $skip) {
 | 
						|
                $message .= json_encode($skip) . '<br/>';
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $message;
 | 
						|
    }
 | 
						|
 | 
						|
    private function checkForFile($fileName)
 | 
						|
    {
 | 
						|
        $counter = 0;
 | 
						|
 | 
						|
        while (! file_exists($fileName)) {
 | 
						|
            $counter++;
 | 
						|
            if ($counter > 60) {
 | 
						|
                throw new Exception('File not found: ' . $fileName);
 | 
						|
            }
 | 
						|
            sleep(2);
 | 
						|
        }
 | 
						|
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
}
 |