1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 21:22:58 +01:00
invoiceninja/app/Import/Providers/BaseImport.php

551 lines
13 KiB
PHP
Raw Normal View History

2022-02-01 07:14:27 +01:00
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Import\Providers;
2022-02-02 05:56:37 +01:00
use App\Factory\ClientFactory;
use App\Factory\InvoiceFactory;
use App\Factory\PaymentFactory;
2022-02-08 05:14:26 +01:00
use App\Factory\QuoteFactory;
2022-02-02 05:56:37 +01:00
use App\Http\Requests\Invoice\StoreInvoiceRequest;
2022-02-08 05:14:26 +01:00
use App\Http\Requests\Quote\StoreQuoteRequest;
2022-02-01 07:14:27 +01:00
use App\Import\ImportException;
2022-02-09 22:53:48 +01:00
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Mail\Import\ImportCompleted;
2022-02-01 07:14:27 +01:00
use App\Models\Company;
2022-02-02 05:56:37 +01:00
use App\Models\Invoice;
2022-02-08 05:14:26 +01:00
use App\Models\Quote;
2022-02-01 07:14:27 +01:00
use App\Models\User;
2022-02-02 05:56:37 +01:00
use App\Repositories\ClientRepository;
2022-02-01 10:04:03 +01:00
use App\Repositories\InvoiceRepository;
2022-02-02 05:56:37 +01:00
use App\Repositories\PaymentRepository;
2022-02-08 05:14:26 +01:00
use App\Repositories\QuoteRepository;
2022-02-01 10:04:03 +01:00
use App\Utils\Traits\CleanLineItems;
2022-02-01 07:14:27 +01:00
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use League\Csv\Reader;
use League\Csv\Statement;
use Symfony\Component\HttpFoundation\ParameterBag;
2022-02-03 00:14:54 +01:00
class BaseImport
{
2022-02-01 10:04:03 +01:00
use CleanLineItems;
2022-02-03 00:14:54 +01:00
2022-02-01 07:14:27 +01:00
public Company $company;
public array $request;
public array $error_array = [];
public $request_name;
2022-02-03 00:14:54 +01:00
public $repository_name;
2022-02-01 07:14:27 +01:00
2022-02-03 00:14:54 +01:00
public $factory_name;
2022-02-01 07:14:27 +01:00
2022-02-03 00:14:54 +01:00
public $repository;
2022-02-01 07:14:27 +01:00
2022-02-03 00:14:54 +01:00
public $transformer;
2022-02-01 07:14:27 +01:00
2022-02-03 00:14:54 +01:00
public function __construct(array $request, Company $company)
{
$this->company = $company;
$this->request = $request;
$this->hash = $request['hash'];
$this->import_type = $request['import_type'];
$this->skip_header = $request['skip_header'] ?? null;
$this->column_map = !empty($request['column_map'])
? array_combine(
array_keys($request['column_map']),
array_column($request['column_map'], 'mapping')
)
: null;
auth()->login($this->company->owner(), true);
auth()
->user()
->setCompany($this->company);
}
2022-02-01 07:14:27 +01:00
2022-02-03 00:14:54 +01:00
protected function getCsvData($entity_type)
{
$base64_encoded_csv = Cache::pull($this->hash . '-' . $entity_type);
if (empty($base64_encoded_csv)) {
2022-02-01 07:14:27 +01:00
return null;
}
2022-02-03 00:14:54 +01:00
$csv = base64_decode($base64_encoded_csv);
$csv = Reader::createFromString($csv);
2022-02-01 07:14:27 +01:00
$stmt = new Statement();
2022-02-03 00:14:54 +01:00
$data = iterator_to_array($stmt->process($csv));
2022-02-01 07:14:27 +01:00
2022-02-03 00:14:54 +01:00
if (count($data) > 0) {
2022-02-01 07:14:27 +01:00
$headers = $data[0];
// Remove Invoice Ninja headers
2022-02-03 00:14:54 +01:00
if (
count($headers) &&
count($data) > 4 &&
$this->import_type === 'csv'
) {
2022-02-01 07:14:27 +01:00
$first_cell = $headers[0];
2022-02-03 00:14:54 +01:00
if (strstr($first_cell, config('ninja.app_name'))) {
array_shift($data); // Invoice Ninja...
array_shift($data); // <blank line>
array_shift($data); // Enitty Type Header
2022-02-01 07:14:27 +01:00
}
}
}
return $data;
}
2022-02-03 00:14:54 +01:00
public function mapCSVHeaderToKeys($csvData)
{
$keys = array_shift($csvData);
2022-02-01 07:14:27 +01:00
2022-02-03 00:14:54 +01:00
return array_map(function ($values) use ($keys) {
return array_combine($keys, $values);
}, $csvData);
2022-02-01 07:14:27 +01:00
}
2022-02-03 00:14:54 +01:00
private function groupInvoices($csvData, $key)
{
2022-02-01 07:14:27 +01:00
// Group by invoice.
$grouped = [];
2022-02-03 00:14:54 +01:00
foreach ($csvData as $line_item) {
if (empty($line_item[$key])) {
$this->error_array['invoice'][] = [
'invoice' => $line_item,
'error' => 'No invoice number',
];
2022-02-01 07:14:27 +01:00
} else {
2022-02-03 00:14:54 +01:00
$grouped[$line_item[$key]][] = $line_item;
2022-02-01 07:14:27 +01:00
}
}
return $grouped;
}
public function getErrors()
{
return $this->error_array;
}
public function ingest($data, $entity_type)
{
2022-02-01 10:04:03 +01:00
$count = 0;
2022-02-10 03:02:02 +01:00
nlog("record count = ".count($data));
2022-02-10 03:38:10 +01:00
nlog($data);
2022-02-03 08:53:39 +01:00
foreach ($data as $key => $record) {
2022-02-03 09:43:28 +01:00
2022-02-10 03:38:10 +01:00
try {
nlog($key);
nlog($record);
2022-02-03 00:14:54 +01:00
$entity = $this->transformer->transform($record);
2022-02-10 03:02:02 +01:00
nlog($entity);
2022-02-03 00:14:54 +01:00
/** @var \App\Http\Requests\Request $request */
$request = new $this->request_name();
// Pass entity data to request so it can be validated
$request->query = $request->request = new ParameterBag($entity);
$validator = Validator::make($entity, $request->rules());
if ($validator->fails()) {
$this->error_array[$entity_type][] = [
$entity_type => $record,
'error' => $validator->errors()->all(),
];
} else {
$entity = $this->repository->save(
array_diff_key($entity, ['user_id' => false]),
$this->factory_name::create(
$this->company->id,
$this->getUserIDForRecord($entity)
)
);
2022-02-10 03:02:02 +01:00
nlog("saving {$entity->name}");
2022-02-03 00:14:54 +01:00
$entity->saveQuietly();
$count++;
2022-02-03 08:53:39 +01:00
2022-02-10 03:38:10 +01:00
nlog("entity number");
nlog($entity->id);
nlog("after save");
2022-02-03 00:14:54 +01:00
}
} catch (\Exception $ex) {
2022-02-03 09:43:28 +01:00
2022-02-10 03:38:10 +01:00
nlog("exception");
nlog($ex->getMessage());
2022-02-10 03:02:02 +01:00
2022-02-03 00:14:54 +01:00
if ($ex instanceof ImportException) {
$message = $ex->getMessage();
} else {
report($ex);
$message = 'Unknown error';
}
2022-02-10 03:38:10 +01:00
nlog($message);
2022-02-03 00:14:54 +01:00
$this->error_array[$entity_type][] = [
$entity_type => $record,
'error' => $message,
];
}
return $count;
}
2022-02-01 07:14:27 +01:00
}
2022-02-03 00:14:54 +01:00
public function ingestInvoices($invoices, $invoice_number_key)
{
2022-02-01 10:04:03 +01:00
$invoice_transformer = $this->transformer;
/** @var PaymentRepository $payment_repository */
2022-02-03 00:14:54 +01:00
$payment_repository = app()->make(PaymentRepository::class);
2022-02-01 10:04:03 +01:00
$payment_repository->import_mode = true;
/** @var ClientRepository $client_repository */
2022-02-03 00:14:54 +01:00
$client_repository = app()->make(ClientRepository::class);
2022-02-01 10:04:03 +01:00
$client_repository->import_mode = true;
2022-02-03 00:14:54 +01:00
$invoice_repository = new InvoiceRepository();
2022-02-01 10:04:03 +01:00
$invoice_repository->import_mode = true;
2022-02-02 05:56:37 +01:00
$invoices = $this->groupInvoices($invoices, $invoice_number_key);
2022-02-03 00:14:54 +01:00
foreach ($invoices as $raw_invoice) {
2022-02-01 10:04:03 +01:00
try {
2022-02-03 00:14:54 +01:00
$invoice_data = $invoice_transformer->transform($raw_invoice);
$invoice_data['line_items'] = $this->cleanItems(
$invoice_data['line_items'] ?? []
);
2022-02-01 10:04:03 +01:00
// If we don't have a client ID, but we do have client data, go ahead and create the client.
2022-02-03 00:14:54 +01:00
if (
empty($invoice_data['client_id']) &&
!empty($invoice_data['client'])
) {
$client_data = $invoice_data['client'];
$client_data['user_id'] = $this->getUserIDForRecord(
$invoice_data
);
2022-02-01 10:04:03 +01:00
$client_repository->save(
$client_data,
2022-02-03 00:14:54 +01:00
$client = ClientFactory::create(
$this->company->id,
$client_data['user_id']
)
2022-02-01 10:04:03 +01:00
);
$invoice_data['client_id'] = $client->id;
2022-02-03 00:14:54 +01:00
unset($invoice_data['client']);
2022-02-01 10:04:03 +01:00
}
2022-02-03 00:14:54 +01:00
$validator = Validator::make(
$invoice_data,
(new StoreInvoiceRequest())->rules()
);
if ($validator->fails()) {
$this->error_array['invoice'][] = [
'invoice' => $invoice_data,
'error' => $validator->errors()->all(),
];
2022-02-01 10:04:03 +01:00
} else {
2022-02-03 00:14:54 +01:00
$invoice = InvoiceFactory::create(
$this->company->id,
$this->getUserIDForRecord($invoice_data)
);
if (!empty($invoice_data['status_id'])) {
2022-02-01 10:04:03 +01:00
$invoice->status_id = $invoice_data['status_id'];
}
2022-02-03 00:14:54 +01:00
$invoice_repository->save($invoice_data, $invoice);
2022-02-01 10:04:03 +01:00
// If we're doing a generic CSV import, only import payment data if we're not importing a payment CSV.
// If we're doing a platform-specific import, trust the platform to only return payment info if there's not a separate payment CSV.
2022-02-03 00:14:54 +01:00
if (
$this->import_type !== 'csv' ||
empty($this->column_map['payment'])
) {
2022-02-01 10:04:03 +01:00
// Check for payment columns
2022-02-03 00:14:54 +01:00
if (!empty($invoice_data['payments'])) {
foreach (
$invoice_data['payments']
as $payment_data
) {
$payment_data['user_id'] = $invoice->user_id;
$payment_data['client_id'] =
$invoice->client_id;
$payment_data['invoices'] = [
2022-02-01 10:04:03 +01:00
[
'invoice_id' => $invoice->id,
2022-02-03 00:14:54 +01:00
'amount' =>
$payment_data['amount'] ?? null,
2022-02-01 10:04:03 +01:00
],
];
/* Make sure we don't apply any payments to invoices with a Zero Amount*/
2022-02-03 00:14:54 +01:00
if ($invoice->amount > 0) {
2022-02-01 10:04:03 +01:00
$payment_repository->save(
$payment_data,
2022-02-03 00:14:54 +01:00
PaymentFactory::create(
$this->company->id,
$invoice->user_id,
$invoice->client_id
)
2022-02-01 10:04:03 +01:00
);
}
}
}
}
2022-02-03 00:14:54 +01:00
$this->actionInvoiceStatus(
$invoice,
$invoice_data,
$invoice_repository
);
2022-02-01 10:04:03 +01:00
}
2022-02-03 00:14:54 +01:00
} catch (\Exception $ex) {
if ($ex instanceof ImportException) {
2022-02-01 10:04:03 +01:00
$message = $ex->getMessage();
} else {
2022-02-03 00:14:54 +01:00
report($ex);
2022-02-01 10:04:03 +01:00
$message = 'Unknown error';
}
2022-02-03 00:14:54 +01:00
$this->error_array['invoice'][] = [
'invoice' => $raw_invoice,
'error' => $message,
];
2022-02-01 10:04:03 +01:00
}
}
}
2022-02-03 00:14:54 +01:00
private function actionInvoiceStatus(
$invoice,
$invoice_data,
$invoice_repository
) {
if (!empty($invoice_data['archived'])) {
$invoice_repository->archive($invoice);
2022-02-02 05:56:37 +01:00
$invoice->fresh();
}
2022-02-01 10:04:03 +01:00
2022-02-03 00:14:54 +01:00
if (!empty($invoice_data['viewed'])) {
$invoice = $invoice
->service()
->markViewed()
->save();
2022-02-02 05:56:37 +01:00
}
2022-02-01 10:04:03 +01:00
2022-02-03 00:14:54 +01:00
if ($invoice->status_id === Invoice::STATUS_DRAFT) {
} elseif ($invoice->status_id === Invoice::STATUS_SENT) {
$invoice = $invoice
->service()
->markSent()
->save();
} elseif (
$invoice->status_id <= Invoice::STATUS_SENT &&
$invoice->amount > 0
) {
if ($invoice->balance <= 0) {
2022-02-02 05:56:37 +01:00
$invoice->status_id = Invoice::STATUS_PAID;
$invoice->save();
2022-02-03 00:14:54 +01:00
} elseif ($invoice->balance != $invoice->amount) {
2022-02-02 05:56:37 +01:00
$invoice->status_id = Invoice::STATUS_PARTIAL;
$invoice->save();
2022-02-03 00:14:54 +01:00
}
2022-02-02 05:56:37 +01:00
}
2022-02-01 10:04:03 +01:00
2022-02-02 05:56:37 +01:00
return $invoice;
}
2022-02-01 10:04:03 +01:00
2022-02-08 05:14:26 +01:00
private function actionQuoteStatus(
$quote,
$quote_data,
$quote_repository
) {
if (!empty($invoice_data['archived'])) {
$quote_repository->archive($quote);
$quote->fresh();
}
if (!empty($invoice_data['viewed'])) {
$quote = $quote
->service()
->markViewed()
->save();
}
if ($quote->status_id === Quote::STATUS_DRAFT) {
} elseif ($quote->status_id === Quote::STATUS_SENT) {
$quote = $quote
->service()
->markSent()
->save();
}
return $quote;
}
public function ingestQuotes($quotes, $quote_number_key)
{
$quote_transformer = $this->transformer;
/** @var ClientRepository $client_repository */
$client_repository = app()->make(ClientRepository::class);
$client_repository->import_mode = true;
$quote_repository = new QuoteRepository();
$quote_repository->import_mode = true;
$quotes = $this->groupInvoices($quotes, $quote_number_key);
foreach ($quotes as $raw_quote) {
try {
$quote_data = $quote_transformer->transform($raw_quote);
$quote_data['line_items'] = $this->cleanItems(
$quote_data['line_items'] ?? []
);
// If we don't have a client ID, but we do have client data, go ahead and create the client.
if (
empty($quote_data['client_id']) &&
!empty($quote_data['client'])
) {
$client_data = $quote_data['client'];
$client_data['user_id'] = $this->getUserIDForRecord(
$quote_data
);
$client_repository->save(
$client_data,
$client = ClientFactory::create(
$this->company->id,
$client_data['user_id']
)
);
$quote_data['client_id'] = $client->id;
unset($quote_data['client']);
}
$validator = Validator::make(
$quote_data,
(new StoreQuoteRequest())->rules()
);
if ($validator->fails()) {
$this->error_array['invoice'][] = [
'quote' => $quote_data,
'error' => $validator->errors()->all(),
];
} else {
$quote = QuoteFactory::create(
$this->company->id,
$this->getUserIDForRecord($quote_data)
);
if (!empty($quote_data['status_id'])) {
$quote->status_id = $quote_data['status_id'];
}
$quote_repository->save($quote_data, $quote);
$this->actionQuoteStatus(
$quote,
$quote_data,
$quote_repository
);
}
} catch (\Exception $ex) {
if ($ex instanceof ImportException) {
$message = $ex->getMessage();
} else {
report($ex);
$message = 'Unknown error';
}
$this->error_array['quote'][] = [
'invoice' => $raw_quote,
'error' => $message,
];
}
}
}
2022-02-03 00:14:54 +01:00
protected function getUserIDForRecord($record)
{
if (!empty($record['user_id'])) {
return $this->findUser($record['user_id']);
} else {
return $this->company->owner()->id;
}
}
2022-02-01 10:04:03 +01:00
2022-02-03 00:14:54 +01:00
protected function findUser($user_hash)
{
$user = User::where('account_id', $this->company->account->id)
->where(
\DB::raw('CONCAT_WS(" ", first_name, last_name)'),
'like',
'%' . $user_hash . '%'
)
->first();
if ($user) {
return $user->id;
} else {
return $this->company->owner()->id;
}
}
2022-02-09 22:53:48 +01:00
protected function finalizeImport()
{
$data = [
'errors' => $this->error_array,
'company' => $this->company,
];
$nmo = new NinjaMailerObject;
$nmo->mailable = new ImportCompleted($this->company, $data);
$nmo->company = $this->company;
$nmo->settings = $this->company->settings;
$nmo->to_user = $this->company->owner();
NinjaMailerJob::dispatch($nmo);
}
2022-02-10 03:02:02 +01:00
public function preTransform(array $data, $entity_type)
{
if (empty($this->column_map[$entity_type])) {
return false;
}
if ($this->skip_header) {
array_shift($data);
}
//sort the array by key
$keys = $this->column_map[$entity_type];
ksort($keys);
$data = array_map(function ($row) use ($keys) {
return array_combine($keys, array_intersect_key($row, $keys));
}, $data);
return $data;
}
2022-02-01 07:14:27 +01:00
}