diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index aa23c649e5..01f7ce12a4 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -14,99 +14,119 @@ namespace App\Http\Controllers; use App\Http\Requests\Import\ImportRequest; use App\Http\Requests\Import\PreImportRequest; use App\Jobs\Import\CSVImport; +use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; use League\Csv\Reader; use League\Csv\Statement; -class ImportController extends Controller -{ +class ImportController extends Controller { - /** - * Store a newly created resource in storage. - * - * @param StoreImportRequest $request - * @return Response - * - * @OA\Post( - * path="/api/v1/preimport", - * operationId="preimport", - * tags={"imports"}, - * summary="Pre Import checks - returns a reference to the job and the headers of the CSV", - * description="Pre Import checks - returns a reference to the job and the headers of the CSV", - * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), - * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), - * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), - * @OA\Parameter(ref="#/components/parameters/include"), - * @OA\RequestBody( - * description="The CSV file", - * required=true, - * @OA\MediaType( - * mediaType="multipart/form-data", - * @OA\Schema( - * type="string", - * format="binary" - * ) - * ) - * ), - * @OA\Response( - * response=200, - * description="Returns a reference to the file", - * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), - * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), - * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), - * ), - * @OA\Response( - * response=422, - * description="Validation error", - * @OA\JsonContent(ref="#/components/schemas/ValidationError"), - * - * ), - * @OA\Response( - * response="default", - * description="Unexpected Error", - * @OA\JsonContent(ref="#/components/schemas/Error"), - * ), - * ) - */ - public function preimport(PreImportRequest $request) - { - //create a reference - $hash = Str::random(32); + /** + * Store a newly created resource in storage. + * + * @param PreImportRequest $request + * + * @return \Illuminate\Http\JsonResponse + * + * @OA\Post( + * path="/api/v1/preimport", + * operationId="preimport", + * tags={"imports"}, + * summary="Pre Import checks - returns a reference to the job and the headers of the CSV", + * description="Pre Import checks - returns a reference to the job and the headers of the CSV", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\RequestBody( + * description="The CSV file", + * required=true, + * @OA\MediaType( + * mediaType="multipart/form-data", + * @OA\Schema( + * type="string", + * format="binary" + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="Returns a reference to the file", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function preimport( PreImportRequest $request ) { + // Create a reference + $hash = Str::random( 32 ); - //store the csv in cache with an expiry of 10 minutes - Cache::put($hash, base64_encode(file_get_contents($request->file('file')->getPathname())), 3600); + $data = [ + 'hash' => $hash, + 'mappings' => [], + ]; + /** @var UploadedFile $file */ + foreach ( $request->files->get( 'files' ) as $entityType => $file ) { + $contents = file_get_contents( $file->getPathname() ); - //parse CSV - $csv_array = $this->getCsvData(file_get_contents($request->file('file')->getPathname())); + // Store the csv in cache with an expiry of 10 minutes + Cache::put( $hash . '-' . $entityType, base64_encode( $contents ), 3600 ); - $class_map = $this->getEntityMap($request->input('entity_type')); + // Parse CSV + $csv_array = $this->getCsvData( $contents ); - $data = [ - 'hash' => $hash, - 'available' => $class_map::importable(), - 'headers' => array_slice($csv_array, 0, 2) - ]; + $class_map = $this->getEntityMap( $entityType ); - return response()->json($data); - } + $data['mappings'][ $entityType ] = [ + 'available' => $class_map::importable(), + 'headers' => array_slice( $csv_array, 0, 2 ), + ]; + } - public function import(ImportRequest $request) - { - CSVImport::dispatch($request->all(), auth()->user()->company()); - - return response()->json(['message' => ctrans('texts.import_started')], 200); - } + return response()->json( $data ); + } - private function getEntityMap($entity_type) - { - return sprintf('App\\Import\\Definitions\%sMap', ucfirst($entity_type)); - } + public function import( ImportRequest $request ) { + $data = $request->all(); - private function getCsvData($csvfile) - { - if (! ini_get('auto_detect_line_endings')) { - ini_set('auto_detect_line_endings', '1'); + if ( empty( $data['hash'] ) ) { + // Create a reference + $data['hash'] = $hash = Str::random( 32 ); + + /** @var UploadedFile $file */ + foreach ( $request->files->get( 'files' ) as $entityType => $file ) { + $contents = file_get_contents( $file->getPathname() ); + + // Store the csv in cache with an expiry of 10 minutes + Cache::put( $hash . '-' . $entityType, base64_encode( $contents ), 3600 ); + } + } + + CSVImport::dispatch( $data, auth()->user()->company() ); + + return response()->json( [ 'message' => ctrans( 'texts.import_started' ) ], 200 ); + } + + private function getEntityMap( $entity_type ) { + return sprintf( 'App\\Import\\Definitions\%sMap', ucfirst( $entity_type ) ); + } + + private function getCsvData( $csvfile ) { + if ( ! ini_get( 'auto_detect_line_endings' ) ) { + ini_set( 'auto_detect_line_endings', '1' ); } $csv = Reader::createFromString($csvfile); @@ -121,10 +141,10 @@ class ImportController extends Controller $firstCell = $headers[0]; if (strstr($firstCell, (string)config('ninja.app_name'))) { - array_shift($data); // Invoice Ninja... - array_shift($data); // - array_shift($data); // Enitty Type Header - } + array_shift( $data ); // Invoice Ninja... + array_shift( $data ); // + array_shift( $data ); // Entity Type Header + } } } diff --git a/app/Http/Requests/Import/ImportRequest.php b/app/Http/Requests/Import/ImportRequest.php index a8564c4abf..334e68e2f4 100644 --- a/app/Http/Requests/Import/ImportRequest.php +++ b/app/Http/Requests/Import/ImportRequest.php @@ -28,10 +28,12 @@ class ImportRequest extends Request public function rules() { return [ - 'hash' => 'required|string', - 'entity_type' => 'required|string', - 'column_map' => 'required|array', - 'skip_header' => 'required|boolean' + 'import_type' => 'required', + 'files' => 'required_without:hash|array|min:1|max:6', + 'hash' => 'nullable|string', + 'column_map' => 'required_with:hash|array', + 'skip_header' => 'required_with:hash|boolean', + 'files.*' => 'file|mimes:csv,txt', ]; } } diff --git a/app/Http/Requests/Import/PreImportRequest.php b/app/Http/Requests/Import/PreImportRequest.php index 75013dfe29..cdeebc0d6a 100644 --- a/app/Http/Requests/Import/PreImportRequest.php +++ b/app/Http/Requests/Import/PreImportRequest.php @@ -28,8 +28,9 @@ class PreImportRequest extends Request public function rules() { return [ - 'file' => 'required|file|mimes:csv,txt', - 'entity_type' => 'required', + 'files.*' => 'file|mimes:csv,txt', + 'files' => 'required|array|min:1|max:6', + 'import_type' => 'required', ]; } } diff --git a/app/Import/Definitions/ExpenseMap.php b/app/Import/Definitions/ExpenseMap.php new file mode 100644 index 0000000000..4f3d0d8860 --- /dev/null +++ b/app/Import/Definitions/ExpenseMap.php @@ -0,0 +1,51 @@ + 'expense.vendor', + 1 => 'expense.client', + 2 => 'expense.project', + 3 => 'expense.category', + 4 => 'expense.amount', + 5 => 'expense.currency', + 6 => 'expense.date', + 7 => 'expense.payment_type', + 8 => 'expense.payment_date', + 9 => 'expense.transaction_reference', + 10 => 'expense.public_notes', + 11 => 'expense.private_notes', + ]; + } + + public static function import_keys() + { + return [ + 0 => 'texts.vendor', + 1 => 'texts.client', + 2 => 'texts.project', + 3 => 'texts.category', + 4 => 'texts.amount', + 5 => 'texts.currency', + 6 => 'texts.date', + 7 => 'texts.payment_type', + 8 => 'texts.payment_date', + 9 => 'texts.transaction_reference', + 10 => 'texts.public_notes', + 11 => 'texts.private_notes', + ]; + } +} diff --git a/app/Import/Definitions/InvoiceMap.php b/app/Import/Definitions/InvoiceMap.php index 9439e36e31..299c0dd84d 100644 --- a/app/Import/Definitions/InvoiceMap.php +++ b/app/Import/Definitions/InvoiceMap.php @@ -26,50 +26,51 @@ class InvoiceMap 7 => 'invoice.date', 8 => 'invoice.due_date', 9 => 'invoice.terms', - 10 => 'invoice.public_notes', - 11 => 'invoice.is_sent', - 12 => 'invoice.private_notes', - 13 => 'invoice.uses_inclusive_taxes', - 14 => 'invoice.tax_name1', - 15 => 'invoice.tax_rate1', - 16 => 'invoice.tax_name2', - 17 => 'invoice.tax_rate2', - 18 => 'invoice.tax_name3', - 19 => 'invoice.tax_rate3', - 20 => 'invoice.is_amount_discount', - 21 => 'invoice.footer', - 22 => 'invoice.partial', - 23 => 'invoice.partial_due_date', - 24 => 'invoice.custom_value1', - 25 => 'invoice.custom_value2', - 26 => 'invoice.custom_value3', - 27 => 'invoice.custom_value4', - 28 => 'invoice.custom_surcharge1', - 29 => 'invoice.custom_surcharge2', - 30 => 'invoice.custom_surcharge3', - 31 => 'invoice.custom_surcharge4', - 32 => 'invoice.exchange_rate', - 33 => 'payment.date', - 34 => 'payment.amount', - 35 => 'payment.transaction_reference', - 36 => 'item.quantity', - 37 => 'item.cost', - 38 => 'item.product_key', - 39 => 'item.notes', - 40 => 'item.discount', - 41 => 'item.is_amount_discount', - 42 => 'item.tax_name1', - 43 => 'item.tax_rate1', - 44 => 'item.tax_name2', - 45 => 'item.tax_rate2', - 46 => 'item.tax_name3', - 47 => 'item.tax_rate3', - 48 => 'item.custom_value1', - 49 => 'item.custom_value2', - 50 => 'item.custom_value3', - 51 => 'item.custom_value4', - 52 => 'item.type_id', - 53 => 'client.email', + 10 => 'invoice.status', + 11 => 'invoice.public_notes', + 12 => 'invoice.is_sent', + 13 => 'invoice.private_notes', + 14 => 'invoice.uses_inclusive_taxes', + 15 => 'invoice.tax_name1', + 16 => 'invoice.tax_rate1', + 17 => 'invoice.tax_name2', + 18 => 'invoice.tax_rate2', + 19 => 'invoice.tax_name3', + 20 => 'invoice.tax_rate3', + 21 => 'invoice.is_amount_discount', + 22 => 'invoice.footer', + 23 => 'invoice.partial', + 24 => 'invoice.partial_due_date', + 25 => 'invoice.custom_value1', + 26 => 'invoice.custom_value2', + 27 => 'invoice.custom_value3', + 28 => 'invoice.custom_value4', + 29 => 'invoice.custom_surcharge1', + 30 => 'invoice.custom_surcharge2', + 31 => 'invoice.custom_surcharge3', + 32 => 'invoice.custom_surcharge4', + 33 => 'invoice.exchange_rate', + 34 => 'payment.date', + 35 => 'payment.amount', + 36 => 'payment.transaction_reference', + 37 => 'item.quantity', + 38 => 'item.cost', + 39 => 'item.product_key', + 40 => 'item.notes', + 41 => 'item.discount', + 42 => 'item.is_amount_discount', + 43 => 'item.tax_name1', + 44 => 'item.tax_rate1', + 45 => 'item.tax_name2', + 46 => 'item.tax_rate2', + 47 => 'item.tax_name3', + 48 => 'item.tax_rate3', + 49 => 'item.custom_value1', + 50 => 'item.custom_value2', + 51 => 'item.custom_value3', + 52 => 'item.custom_value4', + 53 => 'item.type_id', + 54 => 'client.email', ]; } @@ -86,50 +87,51 @@ class InvoiceMap 7 => 'texts.date', 8 => 'texts.due_date', 9 => 'texts.terms', - 10 => 'texts.public_notes', - 11 => 'texts.sent', - 12 => 'texts.private_notes', - 13 => 'texts.uses_inclusive_taxes', - 14 => 'texts.tax_name', - 15 => 'texts.tax_rate', - 16 => 'texts.tax_name', - 17 => 'texts.tax_rate', - 18 => 'texts.tax_name', - 19 => 'texts.tax_rate', - 20 => 'texts.is_amount_discount', - 21 => 'texts.footer', - 22 => 'texts.partial', - 23 => 'texts.partial_due_date', - 24 => 'texts.custom_value1', - 25 => 'texts.custom_value2', - 26 => 'texts.custom_value3', - 27 => 'texts.custom_value4', - 28 => 'texts.surcharge', + 10 => 'texts.status', + 11 => 'texts.public_notes', + 12 => 'texts.sent', + 13 => 'texts.private_notes', + 14 => 'texts.uses_inclusive_taxes', + 15 => 'texts.tax_name', + 16 => 'texts.tax_rate', + 17 => 'texts.tax_name', + 18 => 'texts.tax_rate', + 19 => 'texts.tax_name', + 20 => 'texts.tax_rate', + 21 => 'texts.is_amount_discount', + 22 => 'texts.footer', + 23 => 'texts.partial', + 24 => 'texts.partial_due_date', + 25 => 'texts.custom_value1', + 26 => 'texts.custom_value2', + 27 => 'texts.custom_value3', + 28 => 'texts.custom_value4', 29 => 'texts.surcharge', 30 => 'texts.surcharge', 31 => 'texts.surcharge', - 32 => 'texts.exchange_rate', - 33 => 'texts.payment_date', - 34 => 'texts.payment_amount', - 35 => 'texts.transaction_reference', - 36 => 'texts.quantity', - 37 => 'texts.cost', - 38 => 'texts.product_key', - 39 => 'texts.notes', - 40 => 'texts.discount', - 41 => 'texts.is_amount_discount', - 42 => 'texts.tax_name', - 43 => 'texts.tax_rate', - 44 => 'texts.tax_name', - 45 => 'texts.tax_rate', - 46 => 'texts.tax_name', - 47 => 'texts.tax_rate', - 48 => 'texts.custom_value', + 32 => 'texts.surcharge', + 33 => 'texts.exchange_rate', + 34 => 'texts.payment_date', + 35 => 'texts.payment_amount', + 36 => 'texts.transaction_reference', + 37 => 'texts.quantity', + 38 => 'texts.cost', + 39 => 'texts.product_key', + 40 => 'texts.notes', + 41 => 'texts.discount', + 42 => 'texts.is_amount_discount', + 43 => 'texts.tax_name', + 44 => 'texts.tax_rate', + 45 => 'texts.tax_name', + 46 => 'texts.tax_rate', + 47 => 'texts.tax_name', + 48 => 'texts.tax_rate', 49 => 'texts.custom_value', 50 => 'texts.custom_value', 51 => 'texts.custom_value', - 52 => 'texts.type', - 53 => 'texts.email', + 52 => 'texts.custom_value', + 53 => 'texts.type', + 54 => 'texts.email', ]; } } diff --git a/app/Import/Definitions/VendorMap.php b/app/Import/Definitions/VendorMap.php new file mode 100644 index 0000000000..d44c5570fa --- /dev/null +++ b/app/Import/Definitions/VendorMap.php @@ -0,0 +1,61 @@ + 'vendor.name', + 1 => 'vendor.phone', + 2 => 'vendor.id_number', + 3 => 'vendor.vat_number', + 4 => 'vendor.website', + 5 => 'vendor.first_name', + 6 => 'vendor.last_name', + 7 => 'vendor.email', + 8 => 'vendor.currency_id', + 9 => 'vendor.public_notes', + 10 => 'vendor.private_notes', + 11 => 'vendor.address1', + 12 => 'vendor.address2', + 13 => 'vendor.city', + 14 => 'vendor.state', + 15 => 'vendor.postal_code', + 16 => 'vendor.country_id', + ]; + } + + public static function import_keys() + { + return [ + 0 => 'texts.name', + 1 => 'texts.phone', + 2 => 'texts.id_number', + 3 => 'texts.vat_number', + 4 => 'texts.website', + 5 => 'texts.first_name', + 6 => 'texts.last_name', + 7 => 'texts.email', + 8 => 'texts.currency', + 9 => 'texts.public_notes', + 10 => 'texts.private_notes', + 11 => 'texts.address1', + 12 => 'texts.address2', + 13 => 'texts.city', + 14 => 'texts.state', + 15 => 'texts.postal_code', + 16 => 'texts.country', + ]; + } +} diff --git a/app/Import/ImportException.php b/app/Import/ImportException.php new file mode 100644 index 0000000000..3d9fafdc44 --- /dev/null +++ b/app/Import/ImportException.php @@ -0,0 +1,6 @@ +maps['currencies']->where('code', $code)->first(); + return $this->maps['currencies'][ $code ] ?? $this->maps['company']->settings->currency_id; + } - if ($currency) { - return $currency->id; - } - } + public function getClient($client_name, $client_email) { + $clients = $this->maps['company']->clients; - return $this->maps['company']->settings->currency_id; - } + $clients = $clients->where( 'name', $client_name ); - public function getClient($client_name, $client_email) - { - $clients = $this->maps['company']->clients; + if ( $clients->count() >= 1 ) { + return $clients->first()->id; + } - $clients = $clients->where('name', $client_name); + if ( ! empty( $client_email ) ) { + $contacts = ClientContact::where( 'company_id', $this->maps['company']->id ) + ->where( 'email', $client_email ); - if ($clients->count() >= 1) { - return $clients->first()->id; - } + if ( $contacts->count() >= 1 ) { + return $contacts->first()->client_id; + } + } - - $contacts = ClientContact::where('company_id', $this->maps['company']->id) - ->where('email', $client_email); - - if ($contacts->count() >=1) { - return $contacts->first()->client_id; - } - - return null; - } + return null; + } @@ -101,7 +92,7 @@ class BaseTransformer { $name = trim(strtolower($name)); - return isset($this->maps[ENTITY_CLIENT][$name]); + return isset( $this->maps['client'][ $name ] ); } /** @@ -113,7 +104,7 @@ class BaseTransformer { $name = trim(strtolower($name)); - return isset($this->maps[ENTITY_VENDOR][$name]); + return isset( $this->maps['vendor'][ $name ] ); } @@ -126,7 +117,7 @@ class BaseTransformer { $key = trim(strtolower($key)); - return isset($this->maps[ENTITY_PRODUCT][$key]); + return isset( $this->maps['product'][ $key ] ); } @@ -167,7 +158,7 @@ class BaseTransformer { $name = strtolower(trim($name)); - return isset($this->maps[ENTITY_CLIENT][$name]) ? $this->maps[ENTITY_CLIENT][$name] : null; + return isset( $this->maps['client'][ $name ] ) ? $this->maps['client'][ $name ] : null; } /** @@ -322,7 +313,7 @@ class BaseTransformer */ public function getInvoiceNumber($number) { - return $number ? str_pad(trim($number), 4, '0', STR_PAD_LEFT) : null; + return $number ? ltrim( trim( $number ), '0' ) : null; } /** @@ -334,7 +325,8 @@ class BaseTransformer { $invoiceNumber = $this->getInvoiceNumber($invoiceNumber); $invoiceNumber = strtolower($invoiceNumber); - return isset($this->maps[ENTITY_INVOICE][$invoiceNumber]) ? $this->maps[ENTITY_INVOICE][$invoiceNumber] : null; + + return isset( $this->maps['invoice'][ $invoiceNumber ] ) ? $this->maps['invoice'][ $invoiceNumber ] : null; } /** @@ -346,7 +338,8 @@ class BaseTransformer { $invoiceNumber = $this->getInvoiceNumber($invoiceNumber); $invoiceNumber = strtolower($invoiceNumber); - return isset($this->maps['invoices'][$invoiceNumber]) ? $this->maps['invoices'][$invoiceNumber]->public_id : null; + + return isset( $this->maps['invoice'][ $invoiceNumber ] ) ? $this->maps['invoices'][ $invoiceNumber ]->public_id : null; } /** @@ -359,7 +352,7 @@ class BaseTransformer $invoiceNumber = $this->getInvoiceNumber($invoiceNumber); $invoiceNumber = strtolower($invoiceNumber); - return isset($this->maps[ENTITY_INVOICE][$invoiceNumber]); + return $this->maps['invoice'][ $invoiceNumber ] ?? null; } /** @@ -372,7 +365,7 @@ class BaseTransformer $invoiceNumber = $this->getInvoiceNumber($invoiceNumber); $invoiceNumber = strtolower($invoiceNumber); - return isset($this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber]) ? $this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber] : null; + return $this->maps['invoice_client'][ $invoiceNumber ] ?? null; } /** @@ -384,18 +377,39 @@ class BaseTransformer { $name = strtolower(trim($name)); - return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null; + return $this->maps['vendor'][ $name ] ?? null; } - /** - * @param $name - * - * @return null - */ - public function getExpenseCategoryId($name) - { - $name = strtolower(trim($name)); + /** + * @param $name + * + * @return null + */ + public function getExpenseCategoryId( $name ) { + $name = strtolower( trim( $name ) ); - return isset($this->maps[ENTITY_EXPENSE_CATEGORY][$name]) ? $this->maps[ENTITY_EXPENSE_CATEGORY][$name] : null; - } + return $this->maps['expense_category'][ $name ] ?? null; + } + + /** + * @param $name + * + * @return null + */ + public function getProjectId( $name ) { + $name = strtolower( trim( $name ) ); + + return $this->maps['project'][ $name ] ?? null; + } + + /** + * @param $name + * + * @return null + */ + public function getPaymentTypeId( $name ) { + $name = strtolower( trim( $name ) ); + + return $this->maps['payment_type'][ $name ] ?? null; + } } diff --git a/app/Import/Transformers/ClientTransformer.php b/app/Import/Transformers/ClientTransformer.php deleted file mode 100644 index c12ed0ef84..0000000000 --- a/app/Import/Transformers/ClientTransformer.php +++ /dev/null @@ -1,78 +0,0 @@ -name) && $this->hasClient($data->name)) { - return false; - } - - $settings = new \stdClass; - $settings->currency_id = (string)$this->getCurrencyByCode($data); - - return [ - 'company_id' => $this->maps['company']->id, - 'name' => $this->getString($data, 'client.name'), - 'work_phone' => $this->getString($data, 'client.phone'), - 'address1' => $this->getString($data, 'client.address1'), - 'address2' => $this->getString($data, 'client.address2'), - 'city' => $this->getString($data, 'client.city'), - 'state' => $this->getString($data, 'client.state'), - 'shipping_address1' => $this->getString($data, 'client.shipping_address1'), - 'shipping_address2' => $this->getString($data, 'client.shipping_address2'), - 'shipping_city' => $this->getString($data, 'client.shipping_city'), - 'shipping_state' => $this->getString($data, 'client.shipping_state'), - 'shipping_postal_code' => $this->getString($data, 'client.shipping_postal_code'), - 'public_notes' => $this->getString($data, 'client.public_notes'), - 'private_notes' => $this->getString($data, 'client.private_notes'), - 'website' => $this->getString($data, 'client.website'), - 'vat_number' => $this->getString($data, 'client.vat_number'), - 'id_number' => $this->getString($data, 'client.id_number'), - 'custom_value1' => $this->getString($data, 'client.custom1'), - 'custom_value2' => $this->getString($data, 'client.custom2'), - 'custom_value3' => $this->getString($data, 'client.custom3'), - 'custom_value4' => $this->getString($data, 'client.custom4'), - 'balance' => $this->getFloat($data, 'client.balance'), - 'paid_to_date' => $this->getFloat($data, 'client.paid_to_date'), - 'credit_balance' => 0, - 'settings' => $settings, - 'client_hash' => Str::random(40), - 'contacts' => [ - [ - 'first_name' => $this->getString($data, 'contact.first_name'), - 'last_name' => $this->getString($data, 'contact.last_name'), - 'email' => $this->getString($data, 'contact.email'), - 'phone' => $this->getString($data, 'contact.phone'), - 'custom_value1' => $this->getString($data, 'contact.custom1'), - 'custom_value2' => $this->getString($data, 'contact.custom2'), - 'custom_value3' => $this->getString($data, 'contact.custom3'), - 'custom_value4' => $this->getString($data, 'contact.custom4'), - ], - ], - 'country_id' => isset($data->country_id) ? $this->getCountryId($data->country_id) : null, - 'shipping_country_id' => isset($data->shipping_country_id) ? $this->getCountryId($data->shipping_country_id) : null, - ]; - } -} diff --git a/app/Import/Transformers/Csv/ClientTransformer.php b/app/Import/Transformers/Csv/ClientTransformer.php new file mode 100644 index 0000000000..7e1c1ea6e7 --- /dev/null +++ b/app/Import/Transformers/Csv/ClientTransformer.php @@ -0,0 +1,79 @@ +name) && $this->hasClient($data->name)) { + throw new ImportException('Client already exists'); + } + + $settings = new \stdClass; + $settings->currency_id = (string)$this->getCurrencyByCode($data); + + return [ + 'company_id' => $this->maps['company']->id, + 'name' => $this->getString( $data, 'client.name' ), + 'work_phone' => $this->getString( $data, 'client.phone' ), + 'address1' => $this->getString( $data, 'client.address1' ), + 'address2' => $this->getString( $data, 'client.address2' ), + 'city' => $this->getString( $data, 'client.city' ), + 'state' => $this->getString( $data, 'client.state' ), + 'shipping_address1' => $this->getString( $data, 'client.shipping_address1' ), + 'shipping_address2' => $this->getString( $data, 'client.shipping_address2' ), + 'shipping_city' => $this->getString( $data, 'client.shipping_city' ), + 'shipping_state' => $this->getString( $data, 'client.shipping_state' ), + 'shipping_postal_code' => $this->getString( $data, 'client.shipping_postal_code' ), + 'public_notes' => $this->getString( $data, 'client.public_notes' ), + 'private_notes' => $this->getString( $data, 'client.private_notes' ), + 'website' => $this->getString( $data, 'client.website' ), + 'vat_number' => $this->getString( $data, 'client.vat_number' ), + 'id_number' => $this->getString( $data, 'client.id_number' ), + 'custom_value1' => $this->getString( $data, 'client.custom1' ), + 'custom_value2' => $this->getString( $data, 'client.custom2' ), + 'custom_value3' => $this->getString( $data, 'client.custom3' ), + 'custom_value4' => $this->getString( $data, 'client.custom4' ), + 'balance' => preg_replace( '/[^0-9,.]+/', '', $this->getFloat( $data, 'client.balance' ) ), + 'paid_to_date' => preg_replace( '/[^0-9,.]+/', '', $this->getFloat( $data, 'client.paid_to_date' ) ), + 'credit_balance' => 0, + 'settings' => $settings, + 'client_hash' => Str::random( 40 ), + 'contacts' => [ + [ + 'first_name' => $this->getString( $data, 'contact.first_name' ), + 'last_name' => $this->getString( $data, 'contact.last_name' ), + 'email' => $this->getString( $data, 'contact.email' ), + 'phone' => $this->getString( $data, 'contact.phone' ), + 'custom_value1' => $this->getString( $data, 'contact.custom1' ), + 'custom_value2' => $this->getString( $data, 'contact.custom2' ), + 'custom_value3' => $this->getString( $data, 'contact.custom3' ), + 'custom_value4' => $this->getString( $data, 'contact.custom4' ), + ], + ], + 'country_id' => isset( $data['client.country'] ) ? $this->getCountryId( $data['client.country']) : null, + 'shipping_country_id' => isset($data['client.shipping_country'] ) ? $this->getCountryId( $data['client.shipping_country'] ) : null, + ]; + } +} diff --git a/app/Import/Transformers/Csv/ExpenseTransformer.php b/app/Import/Transformers/Csv/ExpenseTransformer.php new file mode 100644 index 0000000000..5ba18d6816 --- /dev/null +++ b/app/Import/Transformers/Csv/ExpenseTransformer.php @@ -0,0 +1,36 @@ +getClientId( $data['expense.client'] ) : null; + + return [ + 'company_id' => $this->maps['company']->id, + 'amount' => $this->getFloat( $data, 'expense.amount' ), + 'currency_id' => $this->getCurrencyByCode( $data, 'expense.currency_id' ), + 'vendor_id' => isset( $data['expense.vendor'] ) ? $this->getVendorId( $data['expense.vendor'] ) : null, + 'client_id' => isset( $data['expense.client'] ) ? $this->getClientId( $data['expense.client'] ) : null, + 'expense_date' => isset( $data['expense.date'] ) ? date( 'Y-m-d', strtotime( $data['expense.date'] ) ) : null, + 'public_notes' => $this->getString( $data, 'expense.public_notes' ), + 'private_notes' => $this->getString( $data, 'expense.private_notes' ), + 'expense_category_id' => isset( $data['expense.category'] ) ? $this->getExpenseCategoryId( $data['expense.category'] ) : null, + 'project_id' => isset( $data['expense.project'] ) ? $this->getProjectId( $data['expense.project'] ) : null, + 'payment_type_id' => isset( $data['expense.payment_type'] ) ? $this->getPaymentTypeId( $data['expense.payment_type'] ) : null, + 'payment_date' => isset( $data['expense.payment_date'] ) ? date( 'Y-m-d', strtotime( $data['expense.payment_date'] ) ) : null, + 'transaction_reference' => $this->getString( $data, 'expense.transaction_reference' ), + 'should_be_invoiced' => $clientId ? true : false, + ]; + } +} diff --git a/app/Import/Transformers/Csv/InvoiceTransformer.php b/app/Import/Transformers/Csv/InvoiceTransformer.php new file mode 100644 index 0000000000..03ab76c96f --- /dev/null +++ b/app/Import/Transformers/Csv/InvoiceTransformer.php @@ -0,0 +1,131 @@ +hasInvoice( $invoice_data['invoice.number'] ) ) { + throw new ImportException( 'Invoice number already exists' ); + } + + $invoiceStatusMap = [ + 'sent' => Invoice::STATUS_SENT, + 'draft' => Invoice::STATUS_DRAFT, + ]; + + $transformed = [ + 'company_id' => $this->maps['company']->id, + 'number' => $this->getString( $invoice_data, 'invoice.number' ), + 'user_id' => $this->getString( $invoice_data, 'invoice.user_id' ), + 'amount' => $amount = $this->getFloat( $invoice_data, 'invoice.amount' ), + 'balance' => isset( $invoice_data['invoice.balance'] ) ? $this->getFloat( $invoice_data, 'invoice.balance' ) : $amount, + 'client_id' => $this->getClient( $this->getString( $invoice_data, 'client.name' ), $this->getString( $invoice_data, 'client.email' ) ), + 'discount' => $this->getFloat( $invoice_data, 'invoice.discount' ), + 'po_number' => $this->getString( $invoice_data, 'invoice.po_number' ), + 'date' => isset( $invoice_data['invoice.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['invoice.date'] ) ) : null, + 'due_date' => isset( $invoice_data['invoice.due_date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['invoice.due_date'] ) ) : null, + 'terms' => $this->getString( $invoice_data, 'invoice.terms' ), + 'public_notes' => $this->getString( $invoice_data, 'invoice.public_notes' ), + 'is_sent' => $this->getString( $invoice_data, 'invoice.is_sent' ), + 'private_notes' => $this->getString( $invoice_data, 'invoice.private_notes' ), + 'tax_name1' => $this->getString( $invoice_data, 'invoice.tax_name1' ), + 'tax_rate1' => $this->getFloat( $invoice_data, 'invoice.tax_rate1' ), + 'tax_name2' => $this->getString( $invoice_data, 'invoice.tax_name2' ), + 'tax_rate2' => $this->getFloat( $invoice_data, 'invoice.tax_rate2' ), + 'tax_name3' => $this->getString( $invoice_data, 'invoice.tax_name3' ), + 'tax_rate3' => $this->getFloat( $invoice_data, 'invoice.tax_rate3' ), + 'custom_value1' => $this->getString( $invoice_data, 'invoice.custom_value1' ), + 'custom_value2' => $this->getString( $invoice_data, 'invoice.custom_value2' ), + 'custom_value3' => $this->getString( $invoice_data, 'invoice.custom_value3' ), + 'custom_value4' => $this->getString( $invoice_data, 'invoice.custom_value4' ), + 'footer' => $this->getString( $invoice_data, 'invoice.footer' ), + 'partial' => $this->getFloat( $invoice_data, 'invoice.partial' ), + 'partial_due_date' => $this->getString( $invoice_data, 'invoice.partial_due_date' ), + 'custom_surcharge1' => $this->getString( $invoice_data, 'invoice.custom_surcharge1' ), + 'custom_surcharge2' => $this->getString( $invoice_data, 'invoice.custom_surcharge2' ), + 'custom_surcharge3' => $this->getString( $invoice_data, 'invoice.custom_surcharge3' ), + 'custom_surcharge4' => $this->getString( $invoice_data, 'invoice.custom_surcharge4' ), + 'exchange_rate' => $this->getString( $invoice_data, 'invoice.exchange_rate' ), + 'status_id' => $invoiceStatusMap[ $status = + strtolower( $this->getString( $invoice_data, 'invoice.status' ) ) ] ?? + Invoice::STATUS_SENT, + 'viewed' => $status === 'viewed', + 'archived' => $status === 'archived', + ]; + + if ( isset( $invoice_data['payment.amount'] ) ) { + $transformed['payments'] = [ + [ + 'date' => isset( $invoice_data['payment.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['payment.date'] ) ) : date( 'y-m-d' ), + 'transaction_reference' => $this->getString( $invoice_data, 'payment.transaction_reference' ), + 'amount' => $this->getFloat( $invoice_data, 'payment.amount' ), + ], + ]; + } elseif ( $status === 'paid' ) { + $transformed['payments'] = [ + [ + 'date' => isset( $invoice_data['payment.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['payment.date'] ) ) : date( 'y-m-d' ), + 'transaction_reference' => $this->getString( $invoice_data, 'payment.transaction_reference' ), + 'amount' => $this->getFloat( $invoice_data, 'invoice.amount' ), + ], + ]; + } elseif ( isset( $transformed['amount'] ) && isset( $transformed['balance'] ) ) { + $transformed['payments'] = [ + [ + 'date' => isset( $invoice_data['payment.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['payment.date'] ) ) : date( 'y-m-d' ), + 'transaction_reference' => $this->getString( $invoice_data, 'payment.transaction_reference' ), + 'amount' => $transformed['amount'] - $transformed['balance'], + ], + ]; + } + + $line_items = []; + foreach ( $line_items_data as $record ) { + $line_items[] = [ + 'quantity' => $this->getFloat( $record, 'item.quantity' ), + 'cost' => $this->getFloat( $record, 'item.cost' ), + 'product_key' => $this->getString( $record, 'item.product_key' ), + 'notes' => $this->getString( $record, 'item.notes' ), + 'discount' => $this->getFloat( $record, 'item.discount' ), + 'is_amount_discount' => filter_var( $this->getString( $record, 'item.is_amount_discount' ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ), + 'tax_name1' => $this->getString( $record, 'item.tax_name1' ), + 'tax_rate1' => $this->getFloat( $record, 'item.tax_rate1' ), + 'tax_name2' => $this->getString( $record, 'item.tax_name2' ), + 'tax_rate2' => $this->getFloat( $record, 'item.tax_rate2' ), + 'tax_name3' => $this->getString( $record, 'item.tax_name3' ), + 'tax_rate3' => $this->getFloat( $record, 'item.tax_rate3' ), + 'custom_value1' => $this->getString( $record, 'item.custom_value1' ), + 'custom_value2' => $this->getString( $record, 'item.custom_value2' ), + 'custom_value3' => $this->getString( $record, 'item.custom_value3' ), + 'custom_value4' => $this->getString( $record, 'item.custom_value4' ), + 'type_id' => $this->getInvoiceTypeId( $record, 'item.type_id' ), + ]; + } + $transformed['line_items'] = $line_items; + + return $transformed; + } +} diff --git a/app/Import/Transformers/Csv/PaymentTransformer.php b/app/Import/Transformers/Csv/PaymentTransformer.php new file mode 100644 index 0000000000..7f42a1053e --- /dev/null +++ b/app/Import/Transformers/Csv/PaymentTransformer.php @@ -0,0 +1,64 @@ +getClient( $this->getString( $data, 'payment.client_id' ), $this->getString( $data, 'payment.client_id' ) ); + + if ( empty( $client_id ) ) { + throw new ImportException( 'Could not find client.' ); + } + + $transformed = [ + 'company_id' => $this->maps['company']->id, + 'number' => $this->getString( $data, 'payment.number' ), + 'user_id' => $this->getString( $data, 'payment.user_id' ), + 'amount' => $this->getFloat( $data, 'payment.amount' ), + 'refunded' => $this->getFloat( $data, 'payment.refunded' ), + 'applied' => $this->getFloat( $data, 'payment.applied' ), + 'transaction_reference' => $this->getString( $data, 'payment.transaction_reference ' ), + 'date' => $this->getString( $data, 'payment.date' ), + 'private_notes' => $this->getString( $data, 'payment.private_notes' ), + 'custom_value1' => $this->getString( $data, 'payment.custom_value1' ), + 'custom_value2' => $this->getString( $data, 'payment.custom_value2' ), + 'custom_value3' => $this->getString( $data, 'payment.custom_value3' ), + 'custom_value4' => $this->getString( $data, 'payment.custom_value4' ), + 'client_id' => $client_id, + ]; + + + if ( isset( $data['payment.invoice_number'] ) && + $invoice_id = $this->getInvoiceId( $data['payment.invoice_number'] ) ) { + $transformed['invoices'] = [ + [ + 'invoice_id' => $invoice_id, + 'amount' => $transformed['amount'] ?? null, + ], + ]; + } + + return $transformed; + } +} diff --git a/app/Import/Transformers/ProductTransformer.php b/app/Import/Transformers/Csv/ProductTransformer.php similarity index 94% rename from app/Import/Transformers/ProductTransformer.php rename to app/Import/Transformers/Csv/ProductTransformer.php index b9cdee93a1..451c33554a 100644 --- a/app/Import/Transformers/ProductTransformer.php +++ b/app/Import/Transformers/Csv/ProductTransformer.php @@ -9,8 +9,8 @@ * @license https://opensource.org/licenses/AAL */ -namespace App\Import\Transformers; - +namespace App\Import\Transformers\Csv; +use App\Import\Transformers\BaseTransformer; /** * Class ProductTransformer. */ @@ -19,7 +19,7 @@ class ProductTransformer extends BaseTransformer /** * @param $data * - * @return bool|Item + * @return array */ public function transform($data) { diff --git a/app/Import/Transformers/Csv/VendorTransformer.php b/app/Import/Transformers/Csv/VendorTransformer.php new file mode 100644 index 0000000000..639fd4936e --- /dev/null +++ b/app/Import/Transformers/Csv/VendorTransformer.php @@ -0,0 +1,47 @@ +name ) && $this->hasVendor( $data->name ) ) { + throw new ImportException('Vendor already exists'); + } + + return [ + 'company_id' => $this->maps['company']->id, + 'name' => $this->getString( $data, 'vendor.name' ), + 'phone' => $this->getString( $data, 'vendor.phone' ), + 'id_number' => $this->getString( $data, 'vendor.id_number' ), + 'vat_number' => $this->getString( $data, 'vendor.vat_number' ), + 'website' => $this->getString( $data, 'vendor.website' ), + 'currency_id' => $this->getCurrencyByCode( $data, 'vendor.currency_id' ), + 'public_notes' => $this->getString( $data, 'vendor.public_notes' ), + 'private_notes' => $this->getString( $data, 'vendor.private_notes' ), + 'address1' => $this->getString( $data, 'vendor.address1' ), + 'address2' => $this->getString( $data, 'vendor.address2' ), + 'city' => $this->getString( $data, 'vendor.city' ), + 'state' => $this->getString( $data, 'vendor.state' ), + 'postal_code' => $this->getString( $data, 'vendor.postal_code' ), + 'vendor_contacts' => [ + [ + 'first_name' => $this->getString( $data, 'vendor.first_name' ), + 'last_name' => $this->getString( $data, 'vendor.last_name' ), + 'email' => $this->getString( $data, 'vendor.email' ), + 'phone' => $this->getString( $data, 'vendor.phone' ), + ], + ], + 'country_id' => isset( $data['vendor.country_id'] ) ? $this->getCountryId( $data['vendor.country_id'] ) : null, + ]; + } +} diff --git a/app/Import/Transformers/Freshbooks/ClientTransformer.php b/app/Import/Transformers/Freshbooks/ClientTransformer.php new file mode 100644 index 0000000000..b55be7f7fb --- /dev/null +++ b/app/Import/Transformers/Freshbooks/ClientTransformer.php @@ -0,0 +1,55 @@ +hasClient( $data['Organization'] ) ) { + throw new ImportException('Client already exists'); + } + + return [ + 'company_id' => $this->maps['company']->id, + 'name' => $this->getString( $data, 'Organization' ), + 'work_phone' => $this->getString( $data, 'Phone' ), + 'address1' => $this->getString( $data, 'Street' ), + 'city' => $this->getString( $data, 'City' ), + 'state' => $this->getString( $data, 'Province/State' ), + 'postal_code' => $this->getString( $data, 'Postal Code' ), + 'country_id' => isset( $data['Country'] ) ? $this->getCountryId( $data['Country'] ) : null, + 'private_notes' => $this->getString( $data, 'Notes' ), + 'credit_balance' => 0, + 'settings' => new \stdClass, + 'client_hash' => Str::random( 40 ), + 'contacts' => [ + [ + 'first_name' => $this->getString( $data, 'First Name' ), + 'last_name' => $this->getString( $data, 'Last Name' ), + 'email' => $this->getString( $data, 'Email' ), + 'phone' => $this->getString( $data, 'Phone' ), + ], + ], + ]; + } +} diff --git a/app/Import/Transformers/Freshbooks/InvoiceTransformer.php b/app/Import/Transformers/Freshbooks/InvoiceTransformer.php new file mode 100644 index 0000000000..294b2579e0 --- /dev/null +++ b/app/Import/Transformers/Freshbooks/InvoiceTransformer.php @@ -0,0 +1,78 @@ +hasInvoice( $invoice_data['Invoice #'] ) ) { + throw new ImportException( 'Invoice number already exists' ); + } + + $invoiceStatusMap = [ + 'sent' => Invoice::STATUS_SENT, + 'draft' => Invoice::STATUS_DRAFT, + ]; + + $transformed = [ + 'company_id' => $this->maps['company']->id, + 'client_id' => $this->getClient( $this->getString( $invoice_data, 'Client Name' ), null ), + 'number' => $this->getString( $invoice_data, 'Invoice #' ), + 'date' => isset( $invoice_data['Date Issued'] ) ? date( 'Y-m-d', strtotime( $invoice_data['Date Issued'] ) ) : null, + 'currency_id' => $this->getCurrencyByCode( $invoice_data, 'Currency' ), + 'amount' => 0, + 'status_id' => $invoiceStatusMap[ $status = + strtolower( $this->getString( $invoice_data, 'Invoice Status' ) ) ] ?? Invoice::STATUS_SENT, + 'viewed' => $status === 'viewed', + ]; + + $line_items = []; + foreach ( $line_items_data as $record ) { + $line_items[] = [ + 'product_key' => $this->getString( $record, 'Item Name' ), + 'notes' => $this->getString( $record, 'Item Description' ), + 'cost' => $this->getFloat( $record, 'Rate' ), + 'quantity' => $this->getFloat( $record, 'Quantity' ), + 'discount' => $this->getFloat( $record, 'Discount Percentage' ), + 'is_amount_discount' => false, + 'tax_name1' => $this->getString( $record, 'Tax 1 Type' ), + 'tax_rate1' => $this->getFloat( $record, 'Tax 1 Amount' ), + 'tax_name2' => $this->getString( $record, 'Tax 2 Type' ), + 'tax_rate2' => $this->getFloat( $record, 'Tax 2 Amount' ), + ]; + $transformed['amount'] += $this->getFloat( $record, 'Line Total' ); + } + $transformed['line_items'] = $line_items; + + if ( ! empty( $invoice_data['Date Paid'] ) ) { + $transformed['payments'] = [[ + 'date' => date( 'Y-m-d', strtotime( $invoice_data['Date Paid'] ) ), + 'amount' => $transformed['amount'], + ]]; + } + + return $transformed; + } +} diff --git a/app/Import/Transformers/Invoice2Go/InvoiceTransformer.php b/app/Import/Transformers/Invoice2Go/InvoiceTransformer.php new file mode 100644 index 0000000000..0e1e727ce0 --- /dev/null +++ b/app/Import/Transformers/Invoice2Go/InvoiceTransformer.php @@ -0,0 +1,89 @@ +hasInvoice( $invoice_data['DocumentNumber'] ) ) { + throw new ImportException( 'Invoice number already exists' ); + } + + $invoiceStatusMap = [ + 'unsent' => Invoice::STATUS_DRAFT, + 'sent' => Invoice::STATUS_SENT, + ]; + + $transformed = [ + 'company_id' => $this->maps['company']->id, + 'number' => $this->getString( $invoice_data, 'DocumentNumber' ), + 'notes' => $this->getString( $invoice_data, 'Comment' ), + 'date' => isset( $invoice_data['DocumentDate'] ) ? date( 'Y-m-d', strtotime( $invoice_data['DocumentDate'] ) ) : null, + 'currency_id' => $this->getCurrencyByCode( $invoice_data, 'Currency' ), + 'amount' => 0, + 'status_id' => $invoiceStatusMap[ $status = + strtolower( $this->getString( $invoice_data, 'DocumentStatus' ) ) ] ?? Invoice::STATUS_SENT, + 'viewed' => $status === 'viewed', + 'line_items' => [ + [ + 'amount' => $amount = $this->getFloat( $invoice_data, 'TotalAmount' ), + 'quantity' => 1, + 'discount' => $this->getFloat( $invoice_data, 'DiscountValue' ), + 'is_amount_discount' => false, + ], + ], + ]; + + $client_id = + $this->getClient( $this->getString( $invoice_data, 'Name' ), $this->getString( $invoice_data, 'EmailRecipient' ) ); + + if ( $client_id ) { + $transformed['client_id'] = $client_id; + } else { + $transformed['client'] = [ + 'name' => $this->getString( $invoice_data, 'Name' ), + 'address1' => $this->getString( $invoice_data, 'DocumentRecipientAddress' ), + 'shipping_address1' => $this->getString( $invoice_data, 'ShipAddress' ), + 'credit_balance' => 0, + 'settings' => new \stdClass, + 'client_hash' => Str::random( 40 ), + 'contacts' => [ + [ + 'email' => $this->getString( $invoice_data, 'Email' ), + ], + ], + ]; + } + if ( ! empty( $invoice_data['Date Paid'] ) ) { + $transformed['payments'] = [ + [ + 'date' => date( 'Y-m-d', strtotime( $invoice_data['DatePaid'] ) ), + 'amount' => $this->getFloat( $invoice_data, 'Payments' ), + ], + ]; + } + + return $transformed; + } +} diff --git a/app/Import/Transformers/InvoiceItemTransformer.php b/app/Import/Transformers/InvoiceItemTransformer.php deleted file mode 100644 index dfddfe6b46..0000000000 --- a/app/Import/Transformers/InvoiceItemTransformer.php +++ /dev/null @@ -1,46 +0,0 @@ - $this->getFloat($data, 'item.quantity'), - 'cost' => $this->getFloat($data, 'item.cost'), - 'product_key' => $this->getString($data, 'item.product_key'), - 'notes' => $this->getString($data, 'item.notes'), - 'discount' => $this->getFloat($data, 'item.discount'), - 'is_amount_discount' => $this->getString($data, 'item.is_amount_discount'), - 'tax_name1' => $this->getString($data, 'item.tax_name1'), - 'tax_rate1' => $this->getFloat($data, 'item.tax_rate1'), - 'tax_name2' => $this->getString($data, 'item.tax_name2'), - 'tax_rate2' => $this->getFloat($data, 'item.tax_rate2'), - 'tax_name3' => $this->getString($data, 'item.tax_name3'), - 'tax_rate3' => $this->getFloat($data, 'item.tax_rate3'), - 'custom_value1' => $this->getString($data, 'item.custom_value1'), - 'custom_value2' => $this->getString($data, 'item.custom_value2'), - 'custom_value3' => $this->getString($data, 'item.custom_value3'), - 'custom_value4' => $this->getString($data, 'item.custom_value4'), - 'type_id' => $this->getInvoiceTypeId($data, 'item.type_id'), - ]; - } -} diff --git a/app/Import/Transformers/InvoiceTransformer.php b/app/Import/Transformers/InvoiceTransformer.php deleted file mode 100644 index 022388b99d..0000000000 --- a/app/Import/Transformers/InvoiceTransformer.php +++ /dev/null @@ -1,61 +0,0 @@ - $this->maps['company']->id, - 'number' => $this->getString($data, 'invoice.number'), - 'user_id' => $this->getString($data, 'invoice.user_id'), - 'amount' => $this->getFloat($data, 'invoice.amount'), - 'balance' => $this->getFloat($data, 'invoice.balance'), - 'client_id' => $this->getClient($this->getString($data, 'client.name'), $this->getString($data, 'client.email')), - 'discount' => $this->getFloat($data, 'invoice.discount'), - 'po_number' => $this->getString($data, 'invoice.po_number'), - 'date' => $this->getString($data, 'invoice.date'), - 'due_date' => $this->getString($data, 'invoice.due_date'), - 'terms' => $this->getString($data, 'invoice.terms'), - 'public_notes' => $this->getString($data, 'invoice.public_notes'), - 'is_sent' => $this->getString($data, 'invoice.is_sent'), - 'private_notes' => $this->getString($data, 'invoice.private_notes'), - 'tax_name1' => $this->getString($data, 'invoice.tax_name1'), - 'tax_rate1' => $this->getFloat($data, 'invoice.tax_rate1'), - 'tax_name2' => $this->getString($data, 'invoice.tax_name2'), - 'tax_rate2' => $this->getFloat($data, 'invoice.tax_rate2'), - 'tax_name3' => $this->getString($data, 'invoice.tax_name3'), - 'tax_rate3' => $this->getFloat($data, 'invoice.tax_rate3'), - 'custom_value1' => $this->getString($data, 'invoice.custom_value1'), - 'custom_value2' => $this->getString($data, 'invoice.custom_value2'), - 'custom_value3' => $this->getString($data, 'invoice.custom_value3'), - 'custom_value4' => $this->getString($data, 'invoice.custom_value4'), - 'footer' => $this->getString($data, 'invoice.footer'), - 'partial' => $this->getFloat($data, 'invoice.partial'), - 'partial_due_date' => $this->getString($data, 'invoice.partial_due_date'), - 'custom_surcharge1' => $this->getString($data, 'invoice.custom_surcharge1'), - 'custom_surcharge2' => $this->getString($data, 'invoice.custom_surcharge2'), - 'custom_surcharge3' => $this->getString($data, 'invoice.custom_surcharge3'), - 'custom_surcharge4' => $this->getString($data, 'invoice.custom_surcharge4'), - 'exchange_rate' => $this->getString($data, 'invoice.exchange_rate'), - ]; - } -} diff --git a/app/Import/Transformers/Invoicely/ClientTransformer.php b/app/Import/Transformers/Invoicely/ClientTransformer.php new file mode 100644 index 0000000000..b28ab870a9 --- /dev/null +++ b/app/Import/Transformers/Invoicely/ClientTransformer.php @@ -0,0 +1,48 @@ +hasClient( $data['Client Name'] ) ) { + throw new ImportException('Client already exists'); + } + + return [ + 'company_id' => $this->maps['company']->id, + 'name' => $this->getString( $data, 'Client Name' ), + 'work_phone' => $this->getString( $data, 'Phone' ), + 'country_id' => isset( $data['Country'] ) ? $this->getCountryIdBy2( $data['Country'] ) : null, + 'credit_balance' => 0, + 'settings' => new \stdClass, + 'client_hash' => Str::random( 40 ), + 'contacts' => [ + [ + 'email' => $this->getString( $data, 'Email' ), + 'phone' => $this->getString( $data, 'Phone' ), + ], + ], + ]; + } +} diff --git a/app/Import/Transformers/Invoicely/InvoiceTransformer.php b/app/Import/Transformers/Invoicely/InvoiceTransformer.php new file mode 100644 index 0000000000..e48f7a5fd8 --- /dev/null +++ b/app/Import/Transformers/Invoicely/InvoiceTransformer.php @@ -0,0 +1,58 @@ +hasInvoice( $data['Details'] ) ) { + throw new ImportException( 'Invoice number already exists' ); + } + + $transformed = [ + 'company_id' => $this->maps['company']->id, + 'client_id' => $this->getClient( $this->getString( $data, 'Client' ), null ), + 'number' => $this->getString( $data, 'Details' ), + 'date' => isset( $data['Date'] ) ? date( 'Y-m-d', strtotime( $data['Date'] ) ) : null, + 'due_date' => isset( $data['Due'] ) ? date( 'Y-m-d', strtotime( $data['Due'] ) ) : null, + 'status_id' => Invoice::STATUS_SENT, + 'line_items' => [ + [ + 'cost' => $amount = $this->getFloat( $data, 'Total' ), + 'quantity' => 1, + ], + ], + ]; + + if ( strtolower( $data['Status'] ) === 'paid' ) { + $transformed['payments'] = [ + [ + 'date' => date( 'Y-m-d' ), + 'amount' => $amount, + ], + ]; + } + + return $transformed; + } +} diff --git a/app/Import/Transformers/PaymentTransformer.php b/app/Import/Transformers/PaymentTransformer.php deleted file mode 100644 index 126a01a6f3..0000000000 --- a/app/Import/Transformers/PaymentTransformer.php +++ /dev/null @@ -1,46 +0,0 @@ - $this->maps['company']->id, - 'number' => $this->getString($data, 'payment.number'), - 'user_id' => $this->getString($data, 'payment.user_id'), - 'amount' => $this->getFloat($data, 'payment.amount'), - 'refunded' => $this->getFloat($data, 'payment.refunded'), - 'applied' => $this->getFloat($data, 'payment.applied'), - 'transaction_reference' => $this->getString($data, 'payment.transaction_reference '), - 'date' => $this->getString($data, 'payment.date'), - 'private_notes' => $this->getString($data, 'payment.private_notes'), - 'number' => $this->getString($data, 'number'), - 'custom_value1' => $this->getString($data, 'custom_value1'), - 'custom_value2' => $this->getString($data, 'custom_value2'), - 'custom_value3' => $this->getString($data, 'custom_value3'), - 'custom_value4' => $this->getString($data, 'custom_value4'), - 'client_id' => $this->getString($data, 'client_id'), - 'invoice_number' => $this->getString($data, 'payment.invoice_number'), - 'method' => $this - ]; - } -} diff --git a/app/Import/Transformers/Waveaccounting/ClientTransformer.php b/app/Import/Transformers/Waveaccounting/ClientTransformer.php new file mode 100644 index 0000000000..5e91dda83b --- /dev/null +++ b/app/Import/Transformers/Waveaccounting/ClientTransformer.php @@ -0,0 +1,74 @@ +hasClient( $data['customer_name'] ) ) { + throw new ImportException('Client already exists'); + } + + $settings = new \stdClass; + $settings->currency_id = (string) $this->getCurrencyByCode( $data, 'customer_currency' ); + + if ( strval( $data['Payment Terms'] ?? '' ) > 0 ) { + $settings->payment_terms = $data['Payment Terms']; + } + + return [ + 'company_id' => $this->maps['company']->id, + 'name' => $this->getString( $data, 'customer_name' ), + 'number' => $this->getString( $data, 'account_number' ), + 'work_phone' => $this->getString( $data, 'phone' ), + 'website' => $this->getString( $data, 'website' ), + 'country_id' => !empty( $data['country'] ) ? $this->getCountryId( $data['country'] ) : null, + 'state' => $this->getString( $data, 'province/state' ), + 'address1' => $this->getString( $data, 'address_line_1' ), + 'address2' => $this->getString( $data, 'address_line_2' ), + 'city' => $this->getString( $data, 'city' ), + 'postal_code' => $this->getString( $data, 'postal_code/zip_code' ), + + + 'shipping_country_id' => !empty( $data['ship-to_country'] ) ? $this->getCountryId( $data['country'] ) : null, + 'shipping_state' => $this->getString( $data, 'ship-to_province/state' ), + 'shipping_address1' => $this->getString( $data, 'ship-to_address_line_1' ), + 'shipping_address2' => $this->getString( $data, 'ship-to_address_line_2' ), + 'shipping_city' => $this->getString( $data, 'ship-to_city' ), + 'shipping_postal_code' => $this->getString( $data, 'ship-to_postal_code/zip_code' ), + 'public_notes' => $this->getString( $data, 'delivery_instructions' ), + + 'credit_balance' => 0, + 'settings' =>$settings, + 'client_hash' => Str::random( 40 ), + 'contacts' => [ + [ + 'first_name' => $this->getString( $data, 'contact_first_name' ), + 'last_name' => $this->getString( $data, 'contact_last_name' ), + 'email' => $this->getString( $data, 'email' ), + 'phone' => $this->getString( $data, 'phone' ), + ], + ], + ]; + } +} diff --git a/app/Import/Transformers/Waveaccounting/InvoiceTransformer.php b/app/Import/Transformers/Waveaccounting/InvoiceTransformer.php new file mode 100644 index 0000000000..4bf699e8bd --- /dev/null +++ b/app/Import/Transformers/Waveaccounting/InvoiceTransformer.php @@ -0,0 +1,80 @@ +hasInvoice( $invoice_data['Invoice Number'] ) ) { + throw new ImportException( 'Invoice number already exists' ); + } + + $transformed = [ + 'company_id' => $this->maps['company']->id, + 'client_id' => $this->getClient( $customer_name = $this->getString( $invoice_data, 'Customer' ), null ), + 'number' => $invoice_number = $this->getString( $invoice_data, 'Invoice Number' ), + 'date' => isset( $invoice_data['Invoice Date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['Transaction Date'] ) ) : null, + 'currency_id' => $this->getCurrencyByCode( $invoice_data, 'Currency' ), + 'status_id' => Invoice::STATUS_SENT, + ]; + + $line_items = []; + $payments = []; + foreach ( $line_items_data as $record ) { + if ( $record['Account Type'] === 'Income' ) { + $description = $this->getString( $record, 'Transaction Line Description' ); + + // Remove duplicate data from description + if ( substr( $description, 0, strlen( $customer_name ) + 3 ) === $customer_name . ' - ' ) { + $description = substr( $description, strlen( $customer_name ) + 3 ); + } + + if ( substr( $description, 0, strlen( $invoice_number ) + 3 ) === $invoice_number . ' - ' ) { + $description = substr( $description, strlen( $invoice_number ) + 3 ); + } + + $line_items[] = [ + 'notes' => $description, + 'cost' => $this->getFloat( $record, 'Amount Before Sales Tax' ), + 'tax_name1' => $this->getString( $record, 'Sales Tax Name' ), + 'tax_rate1' => $this->getFloat( $record, 'Sales Tax Amount' ), + + 'quantity' => 1, + ]; + } elseif ( $record['Account Type'] === 'System Receivable Invoice' ) { + // This is a payment + $payments[] = [ + 'date' => date( 'Y-m-d', strtotime( $invoice_data['Transaction Date'] ) ), + 'amount' => $this->getFloat( $record, 'Amount (One column)' ), + ]; + } + } + + $transformed['line_items'] = $line_items; + $transformed['payments'] = $payments; + + return $transformed; + } +} diff --git a/app/Import/Transformers/Zoho/ClientTransformer.php b/app/Import/Transformers/Zoho/ClientTransformer.php new file mode 100644 index 0000000000..68efc4825d --- /dev/null +++ b/app/Import/Transformers/Zoho/ClientTransformer.php @@ -0,0 +1,72 @@ +hasClient( $data['Company Name'] ) ) { + throw new ImportException( 'Client already exists' ); + } + + $settings = new \stdClass; + $settings->currency_id = (string) $this->getCurrencyByCode( $data, 'Currency' ); + + if ( strval( $data['Payment Terms'] ?? '' ) > 0 ) { + $settings->payment_terms = $data['Payment Terms']; + } + + return [ + 'company_id' => $this->maps['company']->id, + 'name' => $this->getString( $data, 'Company Name' ), + 'work_phone' => $this->getString( $data, 'Phone' ), + 'private_notes' => $this->getString( $data, 'Notes' ), + 'website' => $this->getString( $data, 'Website' ), + + 'address1' => $this->getString( $data, 'Billing Address' ), + 'address2' => $this->getString( $data, 'Billing Street2' ), + 'city' => $this->getString( $data, 'Billing City' ), + 'state' => $this->getString( $data, 'Billing State' ), + 'postal_code' => $this->getString( $data, 'Billing Code' ), + 'country_id' => isset( $data['Billing Country'] ) ? $this->getCountryId( $data['Billing Country'] ) : null, + + 'shipping_address1' => $this->getString( $data, 'Shipping Address' ), + 'shipping_address2' => $this->getString( $data, 'Shipping Street2' ), + 'shipping_city' => $this->getString( $data, 'Shipping City' ), + 'shipping_state' => $this->getString( $data, 'Shipping State' ), + 'shipping_postal_code' => $this->getString( $data, 'Shipping Code' ), + 'shipping_country_id' => isset( $data['Shipping Country'] ) ? $this->getCountryId( $data['Shipping Country'] ) : null, + 'credit_balance' => 0, + 'settings' => $settings, + 'client_hash' => Str::random( 40 ), + 'contacts' => [ + [ + 'first_name' => $this->getString( $data, 'First Name' ), + 'last_name' => $this->getString( $data, 'Last Name' ), + 'email' => $this->getString( $data, 'Email' ), + 'phone' => $this->getString( $data, 'Phone' ), + ], + ], + ]; + } +} diff --git a/app/Import/Transformers/Zoho/InvoiceTransformer.php b/app/Import/Transformers/Zoho/InvoiceTransformer.php new file mode 100644 index 0000000000..10e4363d16 --- /dev/null +++ b/app/Import/Transformers/Zoho/InvoiceTransformer.php @@ -0,0 +1,77 @@ +hasInvoice( $invoice_data['Invoice Number'] ) ) { + throw new ImportException( 'Invoice number already exists' ); + } + + $invoiceStatusMap = [ + 'sent' => Invoice::STATUS_SENT, + 'draft' => Invoice::STATUS_DRAFT, + ]; + + $transformed = [ + 'company_id' => $this->maps['company']->id, + 'client_id' => $this->getClient( $this->getString( $invoice_data, 'Company Name' ), null ), + 'number' => $this->getString( $invoice_data, 'Invoice Number' ), + 'date' => isset( $invoice_data['Invoice Date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['Invoice Date'] ) ) : null, + 'due_date' => isset( $invoice_data['Due Date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['Due Date'] ) ) : null, + 'po_number' => $this->getString( $invoice_data, 'PurchaseOrder' ), + 'public_notes' => $this->getString( $invoice_data, 'Notes' ), + 'currency_id' => $this->getCurrencyByCode( $invoice_data, 'Currency' ), + 'amount' => $this->getFloat( $invoice_data, 'Total' ), + 'balance' => $this->getFloat( $invoice_data, 'Balance' ), + 'status_id' => $invoiceStatusMap[ $status = + strtolower( $this->getString( $invoice_data, 'Invoice Status' ) ) ] ?? Invoice::STATUS_SENT, + 'viewed' => $status === 'viewed', + ]; + + $line_items = []; + foreach ( $line_items_data as $record ) { + $line_items[] = [ + 'product_key' => $this->getString( $record, 'Item Name' ), + 'notes' => $this->getString( $record, 'Item Description' ), + 'cost' => $this->getFloat( $record, 'Item Price' ), + 'quantity' => $this->getFloat( $record, 'Quantity' ), + 'discount' => $this->getFloat( $record, 'Discount Amount' ), + 'is_amount_discount' => true, + ]; + } + $transformed['line_items'] = $line_items; + + if ( $transformed['balance'] < $transformed['amount'] ) { + $transformed['payments'] = [[ + 'date' => date( 'Y-m-d' ), + 'amount' => $transformed['amount'] - $transformed['balance'], + ]]; + } + + return $transformed; + } +} diff --git a/app/Jobs/Import/CSVImport.php b/app/Jobs/Import/CSVImport.php index d333bde165..fb6fee2b9d 100644 --- a/app/Jobs/Import/CSVImport.php +++ b/app/Jobs/Import/CSVImport.php @@ -13,26 +13,29 @@ namespace App\Jobs\Import; use App\Factory\ClientFactory; use App\Factory\InvoiceFactory; -use App\Factory\ProductFactory; -use App\Http\Requests\Client\StoreClientRequest; +use App\Factory\PaymentFactory; use App\Http\Requests\Invoice\StoreInvoiceRequest; -use App\Http\Requests\Product\StoreProductRequest; -use App\Import\Transformers\ClientTransformer; -use App\Import\Transformers\InvoiceItemTransformer; -use App\Import\Transformers\InvoiceTransformer; -use App\Import\Transformers\ProductTransformer; +use App\Import\ImportException; +use App\Import\Transformers\BaseTransformer; use App\Jobs\Mail\MailRouter; use App\Libraries\MultiDB; use App\Mail\Import\ImportCompleted; use App\Models\Client; +use App\Models\ClientContact; use App\Models\Company; +use App\Models\Country; use App\Models\Currency; +use App\Models\ExpenseCategory; use App\Models\Invoice; +use App\Models\PaymentType; +use App\Models\Product; +use App\Models\Project; +use App\Models\TaxRate; use App\Models\User; -use App\Repositories\ClientContactRepository; +use App\Models\Vendor; use App\Repositories\ClientRepository; use App\Repositories\InvoiceRepository; -use App\Repositories\ProductRepository; +use App\Repositories\PaymentRepository; use App\Utils\Traits\CleanLineItems; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -42,329 +45,547 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Validator; +use Illuminate\Support\Str; use League\Csv\Reader; use League\Csv\Statement; +use Symfony\Component\HttpFoundation\ParameterBag; +use Symfony\Component\HttpFoundation\Request; -class CSVImport implements ShouldQueue -{ - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, CleanLineItems; +class CSVImport implements ShouldQueue { + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, CleanLineItems; - public $invoice; + public $invoice; - public $company; + public $company; - public $hash; + public $hash; - public $entity_type; + public $import_type; - public $skip_header; + public $skip_header; - public $column_map; + public $column_map; - public $import_array; + public $import_array; - public $error_array; + public $error_array = []; - public $maps; + public $maps; - public function __construct(array $request, Company $company) - { - $this->company = $company; + public function __construct( array $request, Company $company ) { + $this->company = $company; + $this->hash = $request['hash']; + $this->import_type = $request['import_type']; + $this->skip_header = $request['skip_header'] ?? null; + $this->column_map = $request['column_map'] ?? null; + } - $this->hash = $request['hash']; + /** + * Execute the job. + * + * + * @return void + */ + public function handle() { - $this->entity_type = $request['entity_type']; + MultiDB::setDb( $this->company->db ); - $this->skip_header = $request['skip_header']; + $this->company->owner()->setCompany( $this->company ); + Auth::login( $this->company->owner(), true ); - $this->column_map = $request['column_map']; - } + $this->buildMaps(); - /** - * Execute the job. - * - * - * @return void - */ - public function handle() - { - MultiDB::setDb($this->company->db); + nlog( "import " . $this->import_type ); + foreach ( [ 'client', 'product', 'invoice', 'payment', 'vendor', 'expense' ] as $entityType ) { + $csvData = $this->getCsvData( $entityType ); - $this->company->owner()->setCompany($this->company); - Auth::login($this->company->owner(), true); + if ( ! empty( $csvData ) ) { + $importFunction = "import" . Str::plural( Str::title( $entityType ) ); + $preTransformFunction = "preTransform" . Str::title( $this->import_type ); - $this->buildMaps(); + if ( method_exists( $this, $preTransformFunction ) ) { + $csvData = $this->$preTransformFunction( $csvData, $entityType ); + } - //sort the array by key - ksort($this->column_map); + if ( empty( $csvData ) ) { + continue; + } - nlog("import".ucfirst($this->entity_type)); - $this->{"import".ucfirst($this->entity_type)}(); - - $data = [ - 'entity' => ucfirst($this->entity_type), - 'errors' => $this->error_array, - 'clients' => $this->maps['clients'], - 'products' => $this->maps['products'], - 'invoices' => $this->maps['invoices'], - 'settings' => $this->company->settings - ]; + if ( method_exists( $this, $importFunction ) ) { + // If there's an entity-specific import function, use that. + $this->$importFunction( $csvData ); + } else { + // Otherwise, use the generic import function. + $this->importEntities( $csvData, $entityType ); + } + } + } - //nlog(print_r($data, 1)); + $data = [ + 'errors' => $this->error_array, + 'company' => $this->company, + ]; - MailRouter::dispatch(new ImportCompleted($data), $this->company, auth()->user()); - } + MailRouter::dispatch( new ImportCompleted( $data ), $this->company, auth()->user() ); + } - public function failed($exception) - { - } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private function preTransformCsv( $csvData, $entityType ) { + if ( empty( $this->column_map[ $entityType ] ) ) { + return false; + } - ////////////////////////////////////////////////////////////////////////////////////////////////////////////// - + if ( $this->skip_header ) { + array_shift( $csvData ); + } + //sort the array by key + $keys = $this->column_map[ $entityType ]; + ksort( $keys ); - private function importInvoice() - { - $invoice_transformer = new InvoiceTransformer($this->maps); + $csvData = array_map( function ( $row ) use ( $keys ) { + return array_combine( $keys, array_intersect_key( $row, $keys ) ); + }, $csvData ); - $records = $this->getCsvData(); + if ( $entityType === 'invoice' ) { + $csvData = $this->groupInvoices( $csvData, 'invoice.number' ); + } - $invoice_number_key = array_search('Invoice Number', reset($records)); + return $csvData; + } - if ($this->skip_header) { - array_shift($records); - } + private function preTransformFreshbooks( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); - if (!$invoice_number_key) { - nlog("no invoice number to use as key - returning"); - return; - } + if ( $entityType === 'invoice' ) { + $csvData = $this->groupInvoices( $csvData, 'Invoice #' ); + } - $unique_invoices = []; - - //get an array of unique invoice numbers - foreach ($records as $key => $value) { - $unique_invoices[] = $value[$invoice_number_key]; - } - - foreach ($unique_invoices as $unique) { - $invoices = array_filter($records, function ($value) use ($invoice_number_key, $unique) { - return $value[$invoice_number_key] == $unique; - }); - - $keys = $this->column_map; - $values = array_intersect_key(reset($invoices), $this->column_map); - $invoice_data = array_combine($keys, $values); - - $invoice = $invoice_transformer->transform($invoice_data); - - $this->processInvoice($invoices, $invoice); - } - } - - private function processInvoice($invoices, $invoice) - { - $invoice_repository = new InvoiceRepository(); - $item_transformer = new InvoiceItemTransformer($this->maps); - $items = []; - - foreach ($invoices as $record) { - $keys = $this->column_map; - $values = array_intersect_key($record, $this->column_map); - $invoice_data = array_combine($keys, $values); - - $items[] = $item_transformer->transform($invoice_data); - } - - $invoice['line_items'] = $this->cleanItems($items); - - $validator = Validator::make($invoice, (new StoreInvoiceRequest())->rules()); - - if ($validator->fails()) { - $this->error_array['invoices'] = ['invoice' => $invoice, 'error' => json_encode($validator->errors())]; - } else { - if ($validator->fails()) { - $this->error_array[] = ['invoice' => $invoice, 'error' => json_encode($validator->errors())]; - } else { - $invoice = $invoice_repository->save($invoice, InvoiceFactory::create($this->company->id, $this->setUser($record))); - - $this->maps['invoices'][] = $invoice->id; - - $this->performInvoiceActions($invoice, $record, $invoice_repository); - } - } - } - - private function performInvoiceActions($invoice, $record, $invoice_repository) - { - $invoice = $this->actionInvoiceStatus($invoice, $record, $invoice_repository); - } - - private function actionInvoiceStatus($invoice, $status, $invoice_repository) - { - switch ($status) { - case 'Archived': - $invoice_repository->archive($invoice); - $invoice->fresh(); - break; - case 'Sent': - $invoice = $invoice->service()->markSent()->save(); - break; - case 'Viewed': - $invoice = $invoice->service()->markSent()->save(); - break; - default: - # code... - break; - } - - if ($invoice->balance < $invoice->amount && $invoice->status_id <= Invoice::STATUS_SENT) { - $invoice->status_id = Invoice::STATUS_PARTIAL; - $invoice->save(); - } - - return $invoice; - } - - //todo limit client imports for hosted version - private function importClient() - { - //clients - $records = $this->getCsvData(); - - $contact_repository = new ClientContactRepository(); - $client_repository = new ClientRepository($contact_repository); - $client_transformer = new ClientTransformer($this->maps); - - if ($this->skip_header) { - array_shift($records); - } - - foreach ($records as $record) { - $keys = $this->column_map; - $values = array_intersect_key($record, $this->column_map); - - $client_data = array_combine($keys, $values); - - $client = $client_transformer->transform($client_data); - - $validator = Validator::make($client, (new StoreClientRequest())->rules()); - - if ($validator->fails()) { - $this->error_array['clients'] = ['client' => $client, 'error' => json_encode($validator->errors())]; - } else { - $client = $client_repository->save($client, ClientFactory::create($this->company->id, $this->setUser($record))); - - if (array_key_exists('client.balance', $client_data)) { - $client->balance = preg_replace('/[^0-9,.]+/', '', $client_data['client.balance']); - } - - if (array_key_exists('client.paid_to_date', $client_data)) { - $client->paid_to_date = preg_replace('/[^0-9,.]+/', '', $client_data['client.paid_to_date']); - } - - $client->save(); - - $this->maps['clients'][] = $client->id; - } - } - } - - - private function importProduct() - { - $product_repository = new ProductRepository(); - $product_transformer = new ProductTransformer($this->maps); - - $records = $this->getCsvData(); - - if ($this->skip_header) { - array_shift($records); - } - - foreach ($records as $record) { - $keys = $this->column_map; - $values = array_intersect_key($record, $this->column_map); - - $product_data = array_combine($keys, $values); - - $product = $product_transformer->transform($product_data); - - $validator = Validator::make($product, (new StoreProductRequest())->rules()); - - if ($validator->fails()) { - $this->error_array['products'] = ['product' => $product, 'error' => json_encode($validator->errors())]; - } else { - $product = $product_repository->save($product, ProductFactory::create($this->company->id, $this->setUser($record))); - - $product->save(); - - $this->maps['products'][] = $product->id; - } - } - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private function buildMaps() - { - $this->maps['currencies'] = Currency::all(); - $this->maps['users'] = $this->company->users; - $this->maps['company'] = $this->company; - $this->maps['clients'] = []; - $this->maps['products'] = []; - $this->maps['invoices'] = []; - - return $this; - } - - - private function setUser($record) - { - $user_key_exists = array_search('client.user_id', $this->column_map); - - if ($user_key_exists) { - return $this->findUser($record[$user_key_exists]); - } else { - return $this->company->owner()->id; - } - } - - private function findUser($user_hash) - { - $user = User::where('company_id', $this->company->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; - } - } - - private function getCsvData() - { - $base64_encoded_csv = Cache::get($this->hash); - $csv = base64_decode($base64_encoded_csv); - $csv = Reader::createFromString($csv); - - $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, config('ninja.app_name'))) { - array_shift($data); // Invoice Ninja... - array_shift($data); // - array_shift($data); // Enitty Type Header - } - } - } - - return $data; - } + return $csvData; + } + + private function preTransformInvoicely( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); + + return $csvData; + } + + private function preTransformInvoice2go( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); + + return $csvData; + } + + private function preTransformZoho( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); + + if ( $entityType === 'invoice' ) { + $csvData = $this->groupInvoices( $csvData, 'Invoice Number' ); + } + + return $csvData; + } + + private function preTransformWaveaccounting( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); + + if ( $entityType === 'invoice' ) { + $csvData = $this->groupInvoices( $csvData, 'Invoice Number' ); + } + + return $csvData; + } + + private function groupInvoices( $csvData, $key ) { + // 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; + } + + private function mapCSVHeaderToKeys( $csvData ) { + $keys = array_shift( $csvData ); + + return array_map( function ( $values ) use ( $keys ) { + return array_combine( $keys, $values ); + }, $csvData ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private function importInvoices( $invoices ) { + $invoice_transformer = $this->getTransformer( 'invoice' ); + + /** @var PaymentRepository $payment_repository */ + $payment_repository = app()->make( PaymentRepository::class ); + /** @var ClientRepository $client_repository */ + $client_repository = app()->make( ClientRepository::class ); + + foreach ( $invoices as $raw_invoice ) { + try { + $invoice_data = $invoice_transformer->transform( $raw_invoice ); + $invoice_repository = new InvoiceRepository(); + + $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 = Validator::make( $invoice_data, ( new StoreInvoiceRequest() )->rules() ); + if ( $validator->fails() ) { + $this->error_array['invoice'][] = + [ 'invoice' => $invoice_data, 'error' => $validator->errors()->all() ]; + } else { + $invoice = InvoiceFactory::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 ); + $this->addInvoiceToMaps( $invoice ); + + // 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, + 'amount' => $payment_data['amount'] ?? null, + ], + ]; + + $payment_repository->save( + $payment_data, + PaymentFactory::create( $this->company->id, $invoice->user_id, $invoice->client_id ) + ); + } + } + } + + $this->actionInvoiceStatus( $invoice, $invoice_data, $invoice_repository ); + } + } catch ( \Exception $ex ) { + if ( $ex instanceof ImportException ) { + $message = $ex->getMessage(); + } else { + report( $ex ); + $message = 'Unknown error'; + } + + $this->error_array['invoice'][] = [ 'invoice' => $raw_invoice, 'error' => $message ]; + } + } + } + + 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_SENT ) { + $invoice = $invoice->service()->markSent()->save(); + } + + if ( $invoice->status_id <= Invoice::STATUS_SENT && $invoice->amount > 0 ) { + if ( $invoice->balance < $invoice->amount ) { + $invoice->status_id = Invoice::STATUS_PARTIAL; + $invoice->save(); + } elseif ( $invoice->balance <= 0 ) { + $invoice->status_id = Invoice::STATUS_PAID; + $invoice->save(); + } + } + + + return $invoice; + } + + private function importEntities( $records, $entity_type ) { + $entity_type = Str::slug( $entity_type, '_' ); + $formatted_entity_type = Str::title( $entity_type ); + + $request_name = "\\App\\Http\\Requests\\${formatted_entity_type}\\Store${formatted_entity_type}Request"; + $repository_name = '\\App\\Repositories\\' . $formatted_entity_type . 'Repository'; + $factoryName = '\\App\\Factory\\' . $formatted_entity_type . 'Factory'; + + $repository = app()->make( $repository_name ); + $transformer = $this->getTransformer( $entity_type ); + + foreach ( $records as $record ) { + try { + $entity = $transformer->transform( $record ); + + /** @var \App\Http\Requests\Request $request */ + $request = new $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 = + $repository->save( + array_diff_key( $entity, [ 'user_id' => false ] ), + $factoryName::create( $this->company->id, $this->getUserIDForRecord( $entity ) ) ); + + $entity->save(); + if ( method_exists( $this, 'add' . $formatted_entity_type . 'ToMaps' ) ) { + $this->{'add' . $formatted_entity_type . 'ToMaps'}( $entity ); + } + } + } catch ( \Exception $ex ) { + if ( $ex instanceof ImportException ) { + $message = $ex->getMessage(); + } else { + report( $ex ); + $message = 'Unknown error'; + } + + $this->error_array[ $entity_type ][] = [ $entity_type => $record, 'error' => $message ]; + } + } + } + + /** + * @param $entity_type + * + * @return BaseTransformer + */ + private function getTransformer( $entity_type ) { + $formatted_entity_type = Str::title( $entity_type ); + $formatted_import_type = Str::title( $this->import_type ); + $transformer_name = + '\\App\\Import\\Transformers\\' . $formatted_import_type . '\\' . $formatted_entity_type . 'Transformer'; + + return new $transformer_name( $this->maps ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private function buildMaps() { + $this->maps = [ + 'company' => $this->company, + 'client' => [], + 'contact' => [], + 'invoice' => [], + 'invoice_client' => [], + 'product' => [], + 'countries' => [], + 'countries2' => [], + 'currencies' => [], + 'client_ids' => [], + 'invoice_ids' => [], + 'vendors' => [], + 'expense_categories' => [], + 'payment_types' => [], + 'tax_rates' => [], + 'tax_names' => [], + ]; + + $clients = Client::scope()->get(); + foreach ( $clients as $client ) { + $this->addClientToMaps( $client ); + } + + $contacts = ClientContact::scope()->get(); + foreach ( $contacts as $contact ) { + $this->addContactToMaps( $contact ); + } + + $invoices = Invoice::scope()->get(); + foreach ( $invoices as $invoice ) { + $this->addInvoiceToMaps( $invoice ); + } + + $products = Product::scope()->get(); + foreach ( $products as $product ) { + $this->addProductToMaps( $product ); + } + + $projects = Project::scope()->get(); + foreach ( $projects as $project ) { + $this->addProjectToMaps( $projects ); + } + + $countries = Country::all(); + foreach ( $countries as $country ) { + $this->maps['countries'][ strtolower( $country->name ) ] = $country->id; + $this->maps['countries2'][ strtolower( $country->iso_3166_2 ) ] = $country->id; + } + + $currencies = Currency::all(); + foreach ( $currencies as $currency ) { + $this->maps['currencies'][ strtolower( $currency->code ) ] = $currency->id; + } + + $payment_types = PaymentType::all(); + foreach ( $payment_types as $payment_type ) { + $this->maps['payment_types'][ strtolower( $payment_type->name ) ] = $payment_type->id; + } + + $vendors = Vendor::scope()->get(); + foreach ( $vendors as $vendor ) { + $this->addVendorToMaps( $vendor ); + } + + $expenseCaegories = ExpenseCategory::scope()->get(); + foreach ( $expenseCaegories as $category ) { + $this->addExpenseCategoryToMaps( $category ); + } + + $taxRates = TaxRate::scope()->get(); + 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->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() ) { + $contact = $client->contacts[0]; + if ( $email = strtolower( trim( $contact->email ) ) ) { + $this->maps['client'][ $email ] = $client->id; + } + if ( $name = strtolower( trim( $contact->first_name . ' ' . $contact->last_name ) ) ) { + $this->maps['client'][ $name ] = $client->id; + } + $this->maps['client_ids'][ $client->public_id ] = $client->id; + } + } + + /** + * @param ClientContact $contact + */ + private function addContactToMaps( ClientContact $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; + } + } + + /** + * @param Project $project + */ + private function addProjectToMaps( Project $project ) { + if ( $key = strtolower( trim( $project->name ) ) ) { + $this->maps['project'][ $key ] = $project; + } + } + + 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 getUserIDForRecord( $record ) { + if ( ! empty( $record['user_id'] ) ) { + return $this->findUser( $record['user_id'] ); + } else { + return $this->company->owner()->id; + } + } + + private function findUser( $user_hash ) { + $user = User::where( 'company_id', $this->company->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; + } + } + + private function getCsvData( $entityType ) { + $base64_encoded_csv = Cache::get( $this->hash . '-' . $entityType ); + if ( empty( $base64_encoded_csv ) ) { + return null; + } + + $csv = base64_decode( $base64_encoded_csv ); + $csv = Reader::createFromString( $csv ); + + $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' ) { + $firstCell = $headers[0]; + if ( strstr( $firstCell, config( 'ninja.app_name' ) ) ) { + array_shift( $data ); // Invoice Ninja... + array_shift( $data ); // + array_shift( $data ); // Enitty Type Header + } + } + } + + return $data; + } } diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 24f281c241..d8a087fb2a 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -20,6 +20,14 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException; use Illuminate\Support\Carbon; + +/** + * Class BaseModel + * + * @method scope() static + * + * @package App\Models + */ class BaseModel extends Model { use MakesHash; diff --git a/app/Models/ClientContact.php b/app/Models/ClientContact.php index b158c3cadc..22e5d68178 100644 --- a/app/Models/ClientContact.php +++ b/app/Models/ClientContact.php @@ -27,6 +27,13 @@ use Illuminate\Notifications\Notifiable; use Illuminate\Support\Facades\Cache; use Laracasts\Presenter\PresentableTrait; +/** + * Class ClientContact + * + * @method scope() static + * + * @package App\Models + */ class ClientContact extends Authenticatable implements HasLocalePreference { use Notifiable; @@ -88,6 +95,27 @@ class ClientContact extends Authenticatable implements HasLocalePreference 'client_id', ]; + + /* + V2 type of scope + */ + public function scopeCompany($query) + { + $query->where('company_id', auth()->user()->companyId()); + + return $query; + } + + /* + V1 type of scope + */ + public function scopeScope($query) + { + $query->where($this->getTable().'.company_id', '=', auth()->user()->company()->id); + + return $query; + } + public function getEntityType() { return self::class; diff --git a/resources/views/email/import/completed.blade.php b/resources/views/email/import/completed.blade.php index 4813e4c007..430ab9120d 100644 --- a/resources/views/email/import/completed.blade.php +++ b/resources/views/email/import/completed.blade.php @@ -1,4 +1,4 @@ -@component('email.template.master', ['design' => 'light', 'settings' => $settings]) +@component('email.template.master', ['design' => 'light', 'settings' => $company->settings]) @slot('header') @include('email.components.header', ['logo' => 'https://www.invoiceninja.com/wp-content/uploads/2015/10/logo-white-horizontal-1.png']) @endslot @@ -74,18 +74,34 @@ @endif

Data Quality:

-

{!! $check_data !!}

+ @if(!empty($errors) ) +

The following import errors occurred:

+ + + + + + + + + + @foreach($errors as $entityType=>$entityErrors) + @foreach($entityErrors as $error) + + + + + + @endforeach + @endforeach + +
TypeDataError
{{$entityType}}{{json_encode($error[$entityType]??null)}}{{json_encode($error['error'])}}
+ @endif + {{ ctrans('texts.account_login')}}

{{ ctrans('texts.email_signature')}}
{{ ctrans('texts.email_from') }}

-@if(!$whitelabel) - @slot('footer') - @component('email.components.footer', ['url' => 'https://invoiceninja.com', 'url_text' => '© InvoiceNinja']) - For any info, please visit InvoiceNinja. - @endcomponent - @endslot -@endif -@endcomponent \ No newline at end of file +@endcomponent