2022-02-01 07:14:27 +01:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* Invoice Ninja (https://invoiceninja.com).
|
|
|
|
*
|
|
|
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
|
|
|
*
|
2024-04-12 06:15:41 +02:00
|
|
|
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
2022-02-01 07:14:27 +01:00
|
|
|
*
|
|
|
|
* @license https://www.elastic.co/licensing/elastic-license
|
|
|
|
*/
|
2022-06-21 11:57:17 +02:00
|
|
|
|
2022-02-01 07:14:27 +01:00
|
|
|
namespace App\Import\Providers;
|
|
|
|
|
2022-02-02 05:56:37 +01:00
|
|
|
use App\Factory\ClientFactory;
|
|
|
|
use App\Factory\InvoiceFactory;
|
|
|
|
use App\Factory\PaymentFactory;
|
2023-10-26 04:57:44 +02:00
|
|
|
use App\Factory\QuoteFactory;
|
|
|
|
use App\Factory\RecurringInvoiceFactory;
|
2023-11-14 03:24:51 +01:00
|
|
|
use App\Factory\TaskFactory;
|
2023-10-26 04:57:44 +02: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;
|
2023-04-19 07:21:50 +02:00
|
|
|
use App\Mail\Import\CsvImportCompleted;
|
2023-10-26 04:57:44 +02:00
|
|
|
use App\Models\Company;
|
|
|
|
use App\Models\Invoice;
|
|
|
|
use App\Models\Quote;
|
|
|
|
use App\Models\User;
|
|
|
|
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;
|
2023-10-26 04:57:44 +02:00
|
|
|
use App\Repositories\QuoteRepository;
|
2023-04-19 07:21:50 +02:00
|
|
|
use App\Repositories\RecurringInvoiceRepository;
|
2023-11-14 03:24:51 +01:00
|
|
|
use App\Repositories\TaskRepository;
|
2023-10-26 04:57:44 +02:00
|
|
|
use App\Utils\Traits\CleanLineItems;
|
|
|
|
use Illuminate\Support\Carbon;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
use Illuminate\Support\Facades\Validator;
|
|
|
|
use League\Csv\Reader;
|
|
|
|
use League\Csv\Statement;
|
2022-02-01 07:14:27 +01:00
|
|
|
|
2022-02-03 00:14:54 +01:00
|
|
|
class BaseImport
|
|
|
|
{
|
2022-06-21 11:57:17 +02:00
|
|
|
use CleanLineItems;
|
2022-02-01 07:14:27 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
public Company $company;
|
2022-02-01 07:14:27 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
public array $request;
|
2022-02-01 07:14:27 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
public array $error_array = [];
|
2022-02-10 03:02:02 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
public $request_name;
|
|
|
|
|
|
|
|
public $repository_name;
|
|
|
|
|
|
|
|
public $factory_name;
|
|
|
|
|
|
|
|
public $repository;
|
|
|
|
|
|
|
|
public $transformer;
|
|
|
|
|
2023-03-01 00:14:25 +01:00
|
|
|
public ?array $column_map = [];
|
|
|
|
|
|
|
|
public ?string $hash;
|
|
|
|
|
|
|
|
public ?string $import_type;
|
|
|
|
|
|
|
|
public ?bool $skip_header;
|
|
|
|
|
2023-04-28 12:16:54 +02:00
|
|
|
public array $entity_count = [];
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
public function __construct(array $request, Company $company)
|
2022-02-10 03:02:02 +01:00
|
|
|
{
|
2022-06-21 11:57:17 +02:00
|
|
|
$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);
|
|
|
|
|
2023-04-28 12:16:54 +02:00
|
|
|
/** @var \App\Models\User $user */
|
|
|
|
$user = auth()->user();
|
|
|
|
|
|
|
|
$user->setCompany($this->company);
|
2022-06-21 11:57:17 +02:00
|
|
|
}
|
2022-02-10 03:02:02 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
public function getCsvData($entity_type)
|
|
|
|
{
|
2023-07-04 01:07:31 +02:00
|
|
|
if (! ini_get('auto_detect_line_endings')) {
|
|
|
|
ini_set('auto_detect_line_endings', '1');
|
|
|
|
}
|
|
|
|
|
2023-08-07 07:07:52 +02:00
|
|
|
/** @var string $base64_encoded_csv */
|
2022-06-21 11:57:17 +02:00
|
|
|
$base64_encoded_csv = Cache::pull($this->hash.'-'.$entity_type);
|
2023-03-01 00:14:25 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
if (empty($base64_encoded_csv)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$csv = base64_decode($base64_encoded_csv);
|
2023-07-22 08:00:57 +02:00
|
|
|
$csv = mb_convert_encoding($csv, 'UTF-8', 'UTF-8');
|
2023-08-06 09:35:19 +02:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
$csv = Reader::createFromString($csv);
|
2023-03-04 00:48:42 +01:00
|
|
|
$csvdelimiter = self::detectDelimiter($csv);
|
2022-06-21 11:57:17 +02:00
|
|
|
|
2023-03-04 00:48:42 +01:00
|
|
|
$csv->setDelimiter($csvdelimiter);
|
2022-06-21 11:57:17 +02:00
|
|
|
$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 &&
|
|
|
|
$this->import_type === 'csv'
|
|
|
|
) {
|
|
|
|
$first_cell = $headers[0];
|
|
|
|
if (strstr($first_cell, config('ninja.app_name'))) {
|
|
|
|
array_shift($data); // Invoice Ninja...
|
|
|
|
array_shift($data); // <blank line>
|
|
|
|
array_shift($data); // Enitty Type Header
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
2023-03-04 00:48:42 +01:00
|
|
|
public function detectDelimiter($csvfile)
|
|
|
|
{
|
2023-03-04 01:53:09 +01:00
|
|
|
$delimiters = [',', '.', ';', '|'];
|
|
|
|
$bestDelimiter = ',';
|
2023-03-04 00:48:42 +01:00
|
|
|
$count = 0;
|
2024-01-10 02:50:01 +01:00
|
|
|
|
|
|
|
// 10-01-2024 - A better way to resolve the csv file delimiter.
|
|
|
|
$csvfile = substr($csvfile, 0, strpos($csvfile, "\n"));
|
|
|
|
|
2023-03-04 00:48:42 +01:00
|
|
|
foreach ($delimiters as $delimiter) {
|
2023-07-04 01:07:31 +02:00
|
|
|
|
|
|
|
if (substr_count(strstr($csvfile, "\n", true), $delimiter) >= $count) {
|
2023-03-04 00:48:42 +01:00
|
|
|
$count = substr_count($csvfile, $delimiter);
|
|
|
|
$bestDelimiter = $delimiter;
|
|
|
|
}
|
2023-07-04 01:07:31 +02:00
|
|
|
|
2023-03-04 00:48:42 +01:00
|
|
|
}
|
2024-02-13 05:25:18 +01:00
|
|
|
|
2024-02-24 03:28:19 +01:00
|
|
|
/** @phpstan-ignore-next-line **/
|
2024-02-07 23:51:40 +01:00
|
|
|
return $bestDelimiter ?? ',';
|
2023-03-04 00:48:42 +01:00
|
|
|
}
|
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
public function mapCSVHeaderToKeys($csvData)
|
|
|
|
{
|
|
|
|
$keys = array_shift($csvData);
|
|
|
|
|
|
|
|
return array_map(function ($values) use ($keys) {
|
|
|
|
return array_combine($keys, $values);
|
|
|
|
}, $csvData);
|
|
|
|
}
|
|
|
|
|
2023-11-14 03:24:51 +01:00
|
|
|
private function groupTasks($csvData, $key)
|
|
|
|
{
|
|
|
|
|
2023-11-20 05:31:40 +01:00
|
|
|
if (! $key || !is_array($csvData) || count($csvData) == 0 || !isset($csvData[0]['task.number']) || empty($csvData[0]['task.number'])) {
|
2023-11-14 03:24:51 +01:00
|
|
|
return $csvData;
|
|
|
|
}
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-11-14 03:24:51 +01:00
|
|
|
// Group by tasks.
|
|
|
|
$grouped = [];
|
|
|
|
|
|
|
|
foreach ($csvData as $item) {
|
|
|
|
if (empty($item[$key])) {
|
|
|
|
$this->error_array['task'][] = [
|
|
|
|
'task' => $item,
|
|
|
|
'error' => 'No task number',
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
$grouped[$item[$key]][] = $item;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $grouped;
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
private function groupInvoices($csvData, $key)
|
|
|
|
{
|
|
|
|
if (! $key) {
|
|
|
|
return $csvData;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Group by invoice.
|
|
|
|
$grouped = [];
|
|
|
|
|
|
|
|
foreach ($csvData as $line_item) {
|
|
|
|
if (empty($line_item[$key])) {
|
|
|
|
$this->error_array['invoice'][] = [
|
|
|
|
'invoice' => $line_item,
|
|
|
|
'error' => 'No invoice number',
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
$grouped[$line_item[$key]][] = $line_item;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $grouped;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getErrors()
|
|
|
|
{
|
|
|
|
return $this->error_array;
|
|
|
|
}
|
|
|
|
|
2022-06-24 03:55:41 +02:00
|
|
|
|
|
|
|
private function runValidation($data)
|
2023-02-16 02:36:09 +01:00
|
|
|
{
|
2024-01-14 05:05:00 +01:00
|
|
|
$_syn_request_class = new $this->request_name();
|
2022-06-24 03:55:41 +02:00
|
|
|
$_syn_request_class->setContainer(app());
|
|
|
|
$_syn_request_class->initialize($data);
|
|
|
|
$_syn_request_class->prepareForValidation();
|
|
|
|
|
|
|
|
$validator = Validator::make($_syn_request_class->all(), $_syn_request_class->rules());
|
|
|
|
|
|
|
|
$_syn_request_class->setValidator($validator);
|
|
|
|
|
|
|
|
return $validator;
|
|
|
|
}
|
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
public function ingest($data, $entity_type)
|
|
|
|
{
|
|
|
|
$count = 0;
|
|
|
|
|
2022-07-27 03:28:30 +02:00
|
|
|
$is_free_hosted_client = $this->company->account->isFreeHostedClient();
|
|
|
|
$hosted_client_count = $this->company->account->hosted_client_count;
|
|
|
|
|
2023-02-16 02:36:09 +01:00
|
|
|
if ($this->factory_name == 'App\Factory\ClientFactory' && $is_free_hosted_client && (count($data) > $hosted_client_count)) {
|
2022-09-10 01:44:43 +02:00
|
|
|
$this->error_array[$entity_type][] = [
|
|
|
|
$entity_type => 'client',
|
|
|
|
'error' => 'Error, you are attempting to import more clients than your plan allows',
|
|
|
|
];
|
|
|
|
|
|
|
|
return $count;
|
|
|
|
}
|
2022-07-27 03:28:30 +02:00
|
|
|
|
2022-09-10 01:44:43 +02:00
|
|
|
foreach ($data as $key => $record) {
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-05-10 03:49:08 +02:00
|
|
|
unset($record['']);
|
|
|
|
|
2024-04-15 00:15:34 +02:00
|
|
|
if(!is_array($record))
|
|
|
|
continue;
|
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
try {
|
|
|
|
$entity = $this->transformer->transform($record);
|
2023-01-12 01:08:32 +01:00
|
|
|
|
2023-02-16 02:36:09 +01:00
|
|
|
if (!$entity) {
|
2023-01-12 01:08:32 +01:00
|
|
|
continue;
|
2023-02-16 02:36:09 +01:00
|
|
|
}
|
2023-01-12 01:08:32 +01:00
|
|
|
|
2022-06-24 03:55:41 +02:00
|
|
|
$validator = $this->runValidation($entity);
|
2022-06-21 11:57:17 +02:00
|
|
|
|
|
|
|
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)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
$entity->saveQuietly();
|
|
|
|
$count++;
|
|
|
|
}
|
|
|
|
} catch (\Exception $ex) {
|
|
|
|
if (\DB::connection(config('database.default'))->transactionLevel() > 0) {
|
|
|
|
\DB::connection(config('database.default'))->rollBack();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($ex instanceof ImportException) {
|
|
|
|
$message = $ex->getMessage();
|
|
|
|
} else {
|
|
|
|
report($ex);
|
|
|
|
$message = 'Unknown error';
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->error_array[$entity_type][] = [
|
|
|
|
$entity_type => $record,
|
|
|
|
'error' => $message,
|
|
|
|
];
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-02-16 02:36:09 +01:00
|
|
|
nlog("Ingest {$ex->getMessage()}");
|
|
|
|
nlog($record);
|
2022-06-21 11:57:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $count;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function ingestProducts($data, $entity_type)
|
|
|
|
{
|
|
|
|
$count = 0;
|
|
|
|
|
|
|
|
foreach ($data as $key => $record) {
|
2024-04-15 00:15:34 +02:00
|
|
|
|
|
|
|
if(!is_array($record)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
try {
|
|
|
|
$entity = $this->transformer->transform($record);
|
|
|
|
$validator = $this->request_name::runFormRequest($entity);
|
|
|
|
|
|
|
|
if ($validator->fails()) {
|
|
|
|
$this->error_array[$entity_type][] = [
|
|
|
|
$entity_type => $record,
|
|
|
|
'error' => $validator->errors()->all(),
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
if ($this->transformer->hasProduct($entity['product_key'])) {
|
|
|
|
$product = $this->transformer->getProduct($entity['product_key']);
|
|
|
|
} else {
|
|
|
|
$product = $this->factory_name::create($this->company->id, $this->getUserIDForRecord($entity));
|
|
|
|
}
|
|
|
|
|
|
|
|
$entity = $this->repository->save(
|
|
|
|
array_diff_key($entity, ['user_id' => false]),
|
|
|
|
$product
|
|
|
|
);
|
|
|
|
|
|
|
|
$entity->saveQuietly();
|
|
|
|
$count++;
|
|
|
|
}
|
|
|
|
} catch (\Exception $ex) {
|
|
|
|
if (\DB::connection(config('database.default'))->transactionLevel() > 0) {
|
|
|
|
\DB::connection(config('database.default'))->rollBack();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($ex instanceof ImportException) {
|
|
|
|
$message = $ex->getMessage();
|
|
|
|
} else {
|
|
|
|
report($ex);
|
|
|
|
$message = 'Unknown error';
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->error_array[$entity_type][] = [
|
|
|
|
$entity_type => $record,
|
|
|
|
'error' => $message,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $count;
|
|
|
|
}
|
2022-02-17 03:27:21 +01:00
|
|
|
|
2023-04-19 07:21:50 +02:00
|
|
|
public function ingestRecurringInvoices($invoices, $invoice_number_key)
|
|
|
|
{
|
|
|
|
$count = 0;
|
|
|
|
|
|
|
|
$invoice_transformer = $this->transformer;
|
|
|
|
|
|
|
|
/** @var ClientRepository $client_repository */
|
|
|
|
$client_repository = app()->make(ClientRepository::class);
|
|
|
|
$client_repository->import_mode = true;
|
|
|
|
|
|
|
|
$invoice_repository = new RecurringInvoiceRepository();
|
|
|
|
$invoice_repository->import_mode = true;
|
|
|
|
|
|
|
|
$invoices = $this->groupInvoices($invoices, $invoice_number_key);
|
|
|
|
|
|
|
|
foreach ($invoices as $raw_invoice) {
|
2024-04-15 00:15:34 +02:00
|
|
|
|
|
|
|
if(!is_array($raw_invoice)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-04-19 07:21:50 +02:00
|
|
|
try {
|
|
|
|
$invoice_data = $invoice_transformer->transform($raw_invoice);
|
|
|
|
|
|
|
|
$invoice_data['line_items'] = $this->cleanItems(
|
|
|
|
$invoice_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($invoice_data['client_id']) &&
|
|
|
|
! empty($invoice_data['client'])
|
|
|
|
) {
|
|
|
|
$client_data = $invoice_data['client'];
|
|
|
|
$client_data['user_id'] = $this->getUserIDForRecord(
|
|
|
|
$invoice_data
|
|
|
|
);
|
|
|
|
|
|
|
|
$client_repository->save(
|
|
|
|
$client_data,
|
|
|
|
$client = ClientFactory::create(
|
|
|
|
$this->company->id,
|
|
|
|
$client_data['user_id']
|
|
|
|
)
|
|
|
|
);
|
|
|
|
$invoice_data['client_id'] = $client->id;
|
|
|
|
unset($invoice_data['client']);
|
|
|
|
}
|
|
|
|
|
|
|
|
$validator = $this->request_name::runFormRequest($invoice_data);
|
|
|
|
|
|
|
|
if ($validator->fails()) {
|
|
|
|
$this->error_array['invoice'][] = [
|
|
|
|
'invoice' => $invoice_data,
|
|
|
|
'error' => $validator->errors()->all(),
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
$invoice = RecurringInvoiceFactory::create(
|
|
|
|
$this->company->id,
|
|
|
|
$this->getUserIDForRecord($invoice_data)
|
|
|
|
);
|
|
|
|
if (! empty($invoice_data['status_id'])) {
|
|
|
|
$invoice->status_id = $invoice_data['status_id'];
|
|
|
|
}
|
|
|
|
$invoice_repository->save($invoice_data, $invoice);
|
|
|
|
|
|
|
|
$count++;
|
|
|
|
// 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.
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-04-19 07:21:50 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
} catch (\Exception $ex) {
|
|
|
|
if (\DB::connection(config('database.default'))->transactionLevel() > 0) {
|
|
|
|
\DB::connection(config('database.default'))->rollBack();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($ex instanceof ImportException) {
|
|
|
|
$message = $ex->getMessage();
|
|
|
|
} else {
|
|
|
|
report($ex);
|
|
|
|
$message = 'Unknown error ';
|
|
|
|
nlog($ex->getMessage());
|
2023-05-04 03:26:53 +02:00
|
|
|
nlog($invoice_data);
|
2023-04-19 07:21:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$this->error_array['recurring_invoice'][] = [
|
|
|
|
'recurring_invoice' => $raw_invoice,
|
|
|
|
'error' => $message,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $count;
|
|
|
|
}
|
|
|
|
|
2023-11-14 03:24:51 +01:00
|
|
|
public function ingestTasks($tasks, $task_number_key)
|
|
|
|
{
|
|
|
|
$count = 0;
|
|
|
|
|
|
|
|
$task_transformer = $this->transformer;
|
|
|
|
|
|
|
|
$task_repository = new TaskRepository();
|
|
|
|
|
|
|
|
$tasks = $this->groupTasks($tasks, $task_number_key);
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-11-14 03:24:51 +01:00
|
|
|
foreach ($tasks as $raw_task) {
|
|
|
|
$task_data = [];
|
2024-04-15 00:15:34 +02:00
|
|
|
|
|
|
|
if(!is_array($raw_task)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-11-14 03:24:51 +01:00
|
|
|
try {
|
|
|
|
$task_data = $task_transformer->transform($raw_task);
|
|
|
|
$task_data['user_id'] = $this->company->owner()->id;
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-11-14 03:24:51 +01:00
|
|
|
$validator = $this->request_name::runFormRequest($task_data);
|
|
|
|
|
|
|
|
if ($validator->fails()) {
|
|
|
|
$this->error_array['task'][] = [
|
|
|
|
'invoice' => $task_data,
|
|
|
|
'error' => $validator->errors()->all(),
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
$task = TaskFactory::create(
|
|
|
|
$this->company->id,
|
|
|
|
$this->company->owner()->id
|
|
|
|
);
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-11-14 03:24:51 +01:00
|
|
|
$task_repository->save($task_data, $task);
|
|
|
|
|
|
|
|
$count++;
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-11-14 03:24:51 +01:00
|
|
|
}
|
|
|
|
} catch (\Exception $ex) {
|
|
|
|
if (\DB::connection(config('database.default'))->transactionLevel() > 0) {
|
|
|
|
\DB::connection(config('database.default'))->rollBack();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($ex instanceof ImportException) {
|
|
|
|
$message = $ex->getMessage();
|
|
|
|
} else {
|
|
|
|
report($ex);
|
|
|
|
$message = 'Unknown error ';
|
|
|
|
nlog($ex->getMessage());
|
|
|
|
nlog($task_data);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->error_array['task'][] = [
|
|
|
|
'task' => $task_data,
|
|
|
|
'error' => $message,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-11-14 03:24:51 +01:00
|
|
|
return $count;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-04-19 07:21:50 +02:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
public function ingestInvoices($invoices, $invoice_number_key)
|
|
|
|
{
|
2023-01-12 01:08:32 +01:00
|
|
|
$count = 0;
|
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
$invoice_transformer = $this->transformer;
|
|
|
|
|
|
|
|
/** @var PaymentRepository $payment_repository */
|
|
|
|
$payment_repository = app()->make(PaymentRepository::class);
|
|
|
|
$payment_repository->import_mode = true;
|
|
|
|
|
|
|
|
/** @var ClientRepository $client_repository */
|
|
|
|
$client_repository = app()->make(ClientRepository::class);
|
|
|
|
$client_repository->import_mode = true;
|
|
|
|
|
|
|
|
$invoice_repository = new InvoiceRepository();
|
|
|
|
$invoice_repository->import_mode = true;
|
|
|
|
|
|
|
|
$invoices = $this->groupInvoices($invoices, $invoice_number_key);
|
|
|
|
|
|
|
|
foreach ($invoices as $raw_invoice) {
|
2024-04-15 00:15:34 +02:00
|
|
|
|
|
|
|
if(!is_array($raw_invoice)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
try {
|
|
|
|
$invoice_data = $invoice_transformer->transform($raw_invoice);
|
2023-05-01 00:07:41 +02:00
|
|
|
$invoice_data['user_id'] = $this->company->owner()->id;
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
$invoice_data['line_items'] = $this->cleanItems(
|
|
|
|
$invoice_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($invoice_data['client_id']) &&
|
|
|
|
! empty($invoice_data['client'])
|
|
|
|
) {
|
|
|
|
$client_data = $invoice_data['client'];
|
|
|
|
$client_data['user_id'] = $this->getUserIDForRecord(
|
|
|
|
$invoice_data
|
|
|
|
);
|
|
|
|
|
|
|
|
$client_repository->save(
|
|
|
|
$client_data,
|
|
|
|
$client = ClientFactory::create(
|
|
|
|
$this->company->id,
|
|
|
|
$client_data['user_id']
|
|
|
|
)
|
|
|
|
);
|
|
|
|
$invoice_data['client_id'] = $client->id;
|
|
|
|
unset($invoice_data['client']);
|
|
|
|
}
|
|
|
|
|
|
|
|
$validator = $this->request_name::runFormRequest($invoice_data);
|
|
|
|
|
|
|
|
if ($validator->fails()) {
|
|
|
|
$this->error_array['invoice'][] = [
|
|
|
|
'invoice' => $invoice_data,
|
|
|
|
'error' => $validator->errors()->all(),
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
$invoice = InvoiceFactory::create(
|
|
|
|
$this->company->id,
|
2023-04-29 01:32:20 +02:00
|
|
|
$this->company->owner()->id
|
2022-06-21 11:57:17 +02:00
|
|
|
);
|
|
|
|
if (! empty($invoice_data['status_id'])) {
|
|
|
|
$invoice->status_id = $invoice_data['status_id'];
|
|
|
|
}
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-05-10 00:58:04 +02:00
|
|
|
nlog($invoice_data);
|
2023-07-20 01:08:17 +02:00
|
|
|
$saveable_invoice_data = $invoice_data;
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-10-26 04:57:44 +02:00
|
|
|
if(array_key_exists('payments', $saveable_invoice_data)) {
|
2023-07-20 01:08:17 +02:00
|
|
|
unset($saveable_invoice_data['payments']);
|
2023-10-26 04:57:44 +02:00
|
|
|
}
|
2023-05-10 00:58:04 +02:00
|
|
|
|
2023-07-20 01:08:17 +02:00
|
|
|
$invoice_repository->save($saveable_invoice_data, $invoice);
|
2022-06-21 11:57:17 +02:00
|
|
|
|
2023-01-12 01:08:32 +01:00
|
|
|
$count++;
|
2022-06-21 11:57:17 +02: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.
|
|
|
|
if (
|
|
|
|
$this->import_type !== 'csv' ||
|
|
|
|
empty($this->column_map['payment'])
|
|
|
|
) {
|
|
|
|
// Check for payment columns
|
|
|
|
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'] = [
|
|
|
|
[
|
|
|
|
'invoice_id' => $invoice->id,
|
2023-02-03 03:29:53 +01:00
|
|
|
'amount' => min($invoice->amount, $payment_data['amount']) ?? null,
|
2022-06-21 11:57:17 +02:00
|
|
|
],
|
|
|
|
];
|
|
|
|
|
|
|
|
/* Make sure we don't apply any payments to invoices with a Zero Amount*/
|
2023-07-20 01:08:17 +02:00
|
|
|
if ($invoice->amount > 0 && $payment_data['amount'] > 0) {
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-04-30 00:19:29 +02:00
|
|
|
$payment = $payment_repository->save(
|
2022-06-21 11:57:17 +02:00
|
|
|
$payment_data,
|
|
|
|
PaymentFactory::create(
|
|
|
|
$this->company->id,
|
|
|
|
$invoice->user_id,
|
|
|
|
$invoice->client_id
|
|
|
|
)
|
|
|
|
);
|
2023-04-30 00:19:29 +02:00
|
|
|
|
|
|
|
$payment_date = Carbon::parse($payment->date);
|
|
|
|
|
2023-10-26 04:57:44 +02:00
|
|
|
if(!$payment_date->isToday()) {
|
2023-04-30 00:19:29 +02:00
|
|
|
|
|
|
|
$payment->paymentables()->update(['created_at' => $payment_date]);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->actionInvoiceStatus(
|
|
|
|
$invoice,
|
|
|
|
$invoice_data,
|
|
|
|
$invoice_repository
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (\Exception $ex) {
|
|
|
|
if (\DB::connection(config('database.default'))->transactionLevel() > 0) {
|
|
|
|
\DB::connection(config('database.default'))->rollBack();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($ex instanceof ImportException) {
|
|
|
|
$message = $ex->getMessage();
|
|
|
|
} else {
|
|
|
|
report($ex);
|
2023-04-13 07:14:54 +02:00
|
|
|
$message = 'Unknown error ';
|
|
|
|
nlog($ex->getMessage());
|
|
|
|
nlog($raw_invoice);
|
2022-06-21 11:57:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$this->error_array['invoice'][] = [
|
|
|
|
'invoice' => $raw_invoice,
|
|
|
|
'error' => $message,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
2023-01-12 01:08:32 +01:00
|
|
|
|
|
|
|
return $count;
|
2022-06-21 11:57:17 +02:00
|
|
|
}
|
2022-02-10 03:02:02 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
private function actionInvoiceStatus(
|
|
|
|
$invoice,
|
|
|
|
$invoice_data,
|
|
|
|
$invoice_repository
|
|
|
|
) {
|
|
|
|
if (! empty($invoice_data['archived'])) {
|
|
|
|
$invoice_repository->archive($invoice);
|
|
|
|
$invoice->fresh();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (! empty($invoice_data['viewed'])) {
|
|
|
|
$invoice = $invoice
|
|
|
|
->service()
|
|
|
|
->markViewed()
|
|
|
|
->save();
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
|
|
|
$invoice->status_id = Invoice::STATUS_PAID;
|
|
|
|
$invoice->save();
|
|
|
|
} elseif ($invoice->balance != $invoice->amount) {
|
|
|
|
$invoice->status_id = Invoice::STATUS_PARTIAL;
|
|
|
|
$invoice->save();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $invoice;
|
2022-02-10 03:02:02 +01:00
|
|
|
}
|
2022-02-17 03:27:21 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
private function actionQuoteStatus(
|
|
|
|
$quote,
|
|
|
|
$quote_data,
|
|
|
|
$quote_repository
|
|
|
|
) {
|
2023-04-28 12:16:54 +02:00
|
|
|
if (! empty($quote_data['archived'])) {
|
2022-06-21 11:57:17 +02:00
|
|
|
$quote_repository->archive($quote);
|
|
|
|
$quote->fresh();
|
|
|
|
}
|
|
|
|
|
2023-04-28 12:16:54 +02:00
|
|
|
if (! empty($quote_data['viewed'])) {
|
2022-06-21 11:57:17 +02:00
|
|
|
$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;
|
|
|
|
}
|
2022-02-17 03:27:21 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
public function ingestQuotes($quotes, $quote_number_key)
|
|
|
|
{
|
2023-01-12 01:08:32 +01:00
|
|
|
$count = 0;
|
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
$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) {
|
2024-04-15 00:15:34 +02:00
|
|
|
|
|
|
|
if(!is_array($raw_quote)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
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);
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-01-12 01:08:32 +01:00
|
|
|
$count++;
|
2022-06-21 11:57:17 +02:00
|
|
|
|
|
|
|
$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,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
2023-01-12 01:08:32 +01:00
|
|
|
|
|
|
|
return $count;
|
2022-06-21 11:57:17 +02:00
|
|
|
}
|
2022-02-17 03:27:21 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
protected function getUserIDForRecord($record)
|
|
|
|
{
|
|
|
|
if (! empty($record['user_id'])) {
|
|
|
|
return $this->findUser($record['user_id']);
|
|
|
|
} else {
|
|
|
|
return $this->company->owner()->id;
|
|
|
|
}
|
|
|
|
}
|
2022-02-17 03:27:21 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
protected function findUser($user_hash)
|
|
|
|
{
|
2023-08-21 07:25:46 +02:00
|
|
|
$user = false;
|
|
|
|
|
2023-08-21 07:05:57 +02:00
|
|
|
if(is_numeric($user_hash)) {
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2023-08-21 07:25:46 +02:00
|
|
|
$user = User::query()
|
|
|
|
->where('account_id', $this->company->account->id)
|
|
|
|
->where('id', $user_hash)
|
|
|
|
->first();
|
2023-08-21 07:05:57 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-10-26 04:57:44 +02:00
|
|
|
if($user) {
|
2023-08-21 07:05:57 +02:00
|
|
|
return $user->id;
|
2023-10-26 04:57:44 +02:00
|
|
|
}
|
2023-08-21 07:05:57 +02:00
|
|
|
|
2023-09-04 02:27:28 +02:00
|
|
|
$user = User::whereRaw("account_id = ? AND CONCAT_WS(' ', first_name, last_name) like ?", [$this->company->account_id, '%'.$user_hash.'%'])
|
2022-06-21 11:57:17 +02:00
|
|
|
->first();
|
|
|
|
|
|
|
|
if ($user) {
|
|
|
|
return $user->id;
|
|
|
|
} else {
|
|
|
|
return $this->company->owner()->id;
|
|
|
|
}
|
|
|
|
}
|
2022-02-17 03:27:21 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
public function finalizeImport()
|
|
|
|
{
|
|
|
|
$data = [
|
|
|
|
'errors' => $this->error_array,
|
|
|
|
'company' => $this->company,
|
2023-01-12 01:08:32 +01:00
|
|
|
'entity_count' => $this->entity_count
|
2022-06-21 11:57:17 +02:00
|
|
|
];
|
|
|
|
|
2024-01-14 05:05:00 +01:00
|
|
|
$nmo = new NinjaMailerObject();
|
2023-01-12 01:08:32 +01:00
|
|
|
$nmo->mailable = new CsvImportCompleted($this->company, $data);
|
2022-06-21 11:57:17 +02:00
|
|
|
$nmo->company = $this->company;
|
|
|
|
$nmo->settings = $this->company->settings;
|
|
|
|
$nmo->to_user = $this->company->owner();
|
|
|
|
|
2022-07-28 02:58:13 +02:00
|
|
|
NinjaMailerJob::dispatch($nmo, true);
|
2022-06-21 11:57:17 +02:00
|
|
|
}
|
2022-02-17 03:27:21 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
public function preTransform(array $data, $entity_type)
|
|
|
|
{
|
|
|
|
$keys = array_shift($data);
|
|
|
|
ksort($keys);
|
2023-07-22 08:00:57 +02:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
return array_map(function ($values) use ($keys) {
|
|
|
|
return array_combine($keys, $values);
|
|
|
|
}, $data);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function preTransformCsv(array $data, $entity_type)
|
|
|
|
{
|
|
|
|
if (empty($this->column_map[$entity_type])) {
|
|
|
|
return false;
|
|
|
|
}
|
2022-02-17 03:27:21 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
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) {
|
2024-01-14 05:05:00 +01:00
|
|
|
|
2024-04-12 00:38:55 +02:00
|
|
|
/** 12-04-2024 If we do not have matching keys - then this row import is _not_ valid */
|
|
|
|
$row_keys = array_keys($row);
|
|
|
|
$key_keys = array_keys($keys);
|
|
|
|
|
|
|
|
$diff = array_diff($key_keys, $row_keys);
|
|
|
|
|
|
|
|
if(!empty($diff)) {
|
|
|
|
return false;
|
2023-03-01 00:14:25 +01:00
|
|
|
}
|
2024-04-12 00:38:55 +02:00
|
|
|
/** 12-04-2024 If we do not have matching keys - then this row import is _not_ valid */
|
2023-03-01 00:14:25 +01:00
|
|
|
|
2022-06-21 11:57:17 +02:00
|
|
|
return array_combine($keys, array_intersect_key($row, $keys));
|
|
|
|
}, $data);
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
}
|
2022-02-01 07:14:27 +01:00
|
|
|
}
|