diff --git a/app/Http/Requests/Import/ImportRequest.php b/app/Http/Requests/Import/ImportRequest.php index dc1074653edf..334e68e2f409 100644 --- a/app/Http/Requests/Import/ImportRequest.php +++ b/app/Http/Requests/Import/ImportRequest.php @@ -30,7 +30,7 @@ class ImportRequest extends Request return [ 'import_type' => 'required', 'files' => 'required_without:hash|array|min:1|max:6', - 'hash' => 'required|string', + 'hash' => 'nullable|string', 'column_map' => 'required_with:hash|array', 'skip_header' => 'required_with:hash|boolean', 'files.*' => 'file|mimes:csv,txt', diff --git a/app/Import/Definitions/ExpenseMap.php b/app/Import/Definitions/ExpenseMap.php new file mode 100644 index 000000000000..4f3d0d88606b --- /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/VendorMap.php b/app/Import/Definitions/VendorMap.php new file mode 100644 index 000000000000..d44c5570facf --- /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 000000000000..3d9fafdc4420 --- /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/Csv/ClientTransformer.php similarity index 85% rename from app/Import/Transformers/ClientTransformer.php rename to app/Import/Transformers/Csv/ClientTransformer.php index 9f1c4f8806c9..7e1c1ea6e772 100644 --- a/app/Import/Transformers/ClientTransformer.php +++ b/app/Import/Transformers/Csv/ClientTransformer.php @@ -9,8 +9,9 @@ * @license https://opensource.org/licenses/AAL */ -namespace App\Import\Transformers; - +namespace App\Import\Transformers\Csv; +use App\Import\ImportException; +use App\Import\Transformers\BaseTransformer; use Illuminate\Support\Str; /** @@ -21,18 +22,18 @@ class ClientTransformer extends BaseTransformer /** * @param $data * - * @return array + * @return array|bool */ public function transform($data) { if (isset($data->name) && $this->hasClient($data->name)) { - return false; + throw new ImportException('Client already exists'); } $settings = new \stdClass; - $settings->currency_id = (string)$this->getCurrencyByCode($data); + $settings->currency_id = (string)$this->getCurrencyByCode($data); - return [ + return [ 'company_id' => $this->maps['company']->id, 'name' => $this->getString( $data, 'client.name' ), 'work_phone' => $this->getString( $data, 'client.phone' ), @@ -71,8 +72,8 @@ class ClientTransformer extends BaseTransformer '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, + '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 000000000000..5ba18d681672 --- /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 000000000000..03ab76c96ff8 --- /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 000000000000..7f42a1053e64 --- /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 b9cdee93a10b..451c33554a09 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 000000000000..639fd4936eb2 --- /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 000000000000..b55be7f7fbdc --- /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 000000000000..294b2579e081 --- /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 000000000000..0e1e727ce09a --- /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 dfddfe6b46c8..000000000000 --- 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 fb1603e3c41c..000000000000 --- 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' => $amount = $this->getFloat($data, 'invoice.amount'), - 'balance' => isset( $data['invoice.balance'] ) ? $this->getFloat( $data, 'invoice.balance' ) : $amount, - '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 000000000000..b28ab870a913 --- /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 000000000000..e48f7a5fd8cd --- /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 126a01a6f3a8..000000000000 --- 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 000000000000..5e91dda83b79 --- /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 000000000000..4bf699e8bd72 --- /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 000000000000..68efc4825d55 --- /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 000000000000..10e4363d16fe --- /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 51a631975bbf..d908665aa2ed 100644 --- a/app/Jobs/Import/CSVImport.php +++ b/app/Jobs/Import/CSVImport.php @@ -14,16 +14,9 @@ namespace App\Jobs\Import; use App\Factory\ClientFactory; use App\Factory\InvoiceFactory; use App\Factory\PaymentFactory; -use App\Factory\ProductFactory; -use App\Http\Requests\Client\StoreClientRequest; use App\Http\Requests\Invoice\StoreInvoiceRequest; -use App\Http\Requests\Product\StoreProductRequest; +use App\Import\ImportException; use App\Import\Transformers\BaseTransformer; -use App\Import\Transformers\ClientTransformer; -use App\Import\Transformers\InvoiceItemTransformer; -use App\Import\Transformers\InvoiceTransformer; -use App\Import\Transformers\PaymentTransformer; -use App\Import\Transformers\ProductTransformer; use App\Jobs\Mail\MailRouter; use App\Libraries\MultiDB; use App\Mail\Import\ImportCompleted; @@ -32,14 +25,15 @@ use App\Models\ClientContact; use App\Models\Company; use App\Models\Country; use App\Models\Currency; -use App\Models\Expense; use App\Models\ExpenseCategory; use App\Models\Invoice; -use App\Models\Payment; +use App\Models\PaymentType; use App\Models\Product; +use App\Models\Project; use App\Models\TaxRate; use App\Models\User; use App\Models\Vendor; +use App\Repositories\ClientRepository; use App\Repositories\InvoiceRepository; use App\Repositories\PaymentRepository; use App\Utils\Traits\CleanLineItems; @@ -54,6 +48,8 @@ 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; @@ -77,15 +73,11 @@ class CSVImport implements ShouldQueue { public $maps; public function __construct( array $request, Company $company ) { - $this->company = $company; - - $this->hash = $request['hash']; - + $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->column_map = $request['column_map'] ?? null; } /** @@ -103,35 +95,21 @@ class CSVImport implements ShouldQueue { $this->buildMaps(); - //sort the array by key - foreach ( $this->column_map as $entityType => &$map ) { - ksort( $map ); - } - - nlog( "import" . ucfirst( $this->import_type ) ); - $this->{"import" . ucfirst( $this->import_type )}(); - - $data = [ - 'errors' => $this->error_array, - 'company'=>$this->company, - ]; - - - MailRouter::dispatchNow( new ImportCompleted( $data ), $this->company, auth()->user() ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - private function importCsv() { + nlog( "import " . $this->import_type ); foreach ( [ 'client', 'product', 'invoice', 'payment', 'vendor', 'expense' ] as $entityType ) { - if ( empty( $this->column_map[ $entityType ] ) ) { - continue; - } - $csvData = $this->getCsvData( $entityType ); if ( ! empty( $csvData ) ) { - $importFunction = "import" . Str::plural( Str::title( $entityType ) ); + $importFunction = "import" . Str::plural( Str::title( $entityType ) ); + $preTransformFunction = "preTransform" . Str::title( $this->import_type ); + + if ( method_exists( $this, $preTransformFunction ) ) { + $csvData = $this->$preTransformFunction( $csvData, $entityType ); + } + + if ( empty( $csvData ) ) { + continue; + } if ( method_exists( $this, $importFunction ) ) { // If there's an entity-specific import function, use that. @@ -142,113 +120,204 @@ class CSVImport implements ShouldQueue { } } } + + $data = [ + 'errors' => $this->error_array, + 'company' => $this->company, + ]; + + MailRouter::dispatchNow( new ImportCompleted( $data ), $this->company, auth()->user() ); } - private function importInvoices( $records ) { - $invoice_transformer = new InvoiceTransformer( $this->maps ); + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + private function preTransformCsv( $csvData, $entityType ) { + if ( empty( $this->column_map[ $entityType ] ) ) { + return false; + } if ( $this->skip_header ) { - array_shift( $records ); + array_shift( $csvData ); } - $keys = $this->column_map['invoice']; - $invoice_number_key = array_search( 'invoice.number', $keys ); - if ( $invoice_number_key === false ) { - nlog( "no invoice number to use as key - returning" ); + //sort the array by key + $keys = $this->column_map[ $entityType ]; + ksort( $keys ); - return; + $csvData = array_map( function ( $row ) use ( $keys ) { + return array_combine( $keys, array_intersect_key( $row, $keys ) ); + }, $csvData ); + + if ( $entityType === 'invoice' ) { + $csvData = $this->groupInvoices( $csvData, 'invoice.number' ); } - $items_by_invoice = []; - - // Group line items by invoice and map columns to keys. - foreach ( $records as $key => $value ) { - $items_by_invoice[ $value[ $invoice_number_key ] ][] = array_combine( $keys,array_intersect_key( $value , $keys )); - } - - foreach ( $items_by_invoice as $invoice_number => $line_items ) { - $invoice_data = array_combine( $keys, reset( $line_items ) ); - - $invoice = $invoice_transformer->transform( $invoice_data ); - - $this->processInvoice( $line_items, $invoice ); - } + return $csvData; } - private function processInvoice( $line_items, $invoice ) { - $invoice_repository = new InvoiceRepository(); - $item_transformer = new InvoiceItemTransformer( $this->maps ); - $items = []; + private function preTransformFreshbooks( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); - foreach ( $line_items as $record ) { - $items[] = $item_transformer->transform( $record ); + if ( $entityType === 'invoice' ) { + $csvData = $this->groupInvoices( $csvData, 'Invoice #' ); } - $invoice['line_items'] = $this->cleanItems( $items ); + return $csvData; + } - $validator = Validator::make( $invoice, ( new StoreInvoiceRequest() )->rules() ); + private function preTransformInvoicely( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); - if ( $validator->fails() ) { - $this->error_array['invoice'][] = [ 'invoice' => $invoice, 'error' => $validator->errors()->all() ]; - } else { - $invoice = - $invoice_repository->save( $invoice, InvoiceFactory::create( $this->company->id, $this->getUserIDForRecord( $record ) ) ); + return $csvData; + } - $this->addInvoiceToMaps( $invoice ); + private function preTransformInvoice2go( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); - // If there's no payment import, try importing payment data from the invoices CSV. - if ( empty( $this->column_map['payment'] ) ) { - $payment_data = reset( $line_items ); - // Check for payment columns - if ( ! empty( $payment_data['payment.amount'] ) ) { - // Transform the payment to be saved - $payment_transformer = new PaymentTransformer( $this->maps ); + return $csvData; + } - /** @var PaymentRepository $payment_repository */ - $payment_repository = app()->make( PaymentRepository::class ); - $transformed_payment = $payment_transformer->transform( $payment_data ); - $transformed_payment['user_id'] = $invoice->user_id; - $transformed_payment['client_id'] = $invoice->client_id; - $transformed_payment['invoices'] = [ - [ - 'invoice_id' => $invoice->id, - 'amount' => $transformed_payment['amount'], - ], - ]; + private function preTransformZoho( $csvData, $entityType ) { + $csvData = $this->mapCSVHeaderToKeys( $csvData ); - $payment_repository->save( - $transformed_payment, - PaymentFactory::create( $this->company->id, $invoice->user_id, $invoice->client_id ) - ); - } + 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; } + } - $this->actionInvoiceStatus( $invoice, $record['invoice.status']??null, $invoice_repository ); + 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, $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; + private function actionInvoiceStatus( $invoice, $invoice_data, $invoice_repository ) { + if ( ! empty( $invoice_data['archived'] ) ) { + $invoice_repository->archive( $invoice ); + $invoice->fresh(); } - if($invoice->status_id <= Invoice::STATUS_SENT){ - if ( $invoice->balance < $invoice->amount) { + 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){ + } elseif ( $invoice->balance <= 0 ) { $invoice->status_id = Invoice::STATUS_PAID; $invoice->save(); } @@ -262,41 +331,65 @@ class CSVImport implements ShouldQueue { $entity_type = Str::slug( $entity_type, '_' ); $formatted_entity_type = Str::title( $entity_type ); - $request = "\\App\\Http\\Requests\\${formatted_entity_type}\\Store${formatted_entity_type}Request"; - $repository_name = '\\App\\Repositories\\'.$formatted_entity_type . 'Repository'; - $transformer_name = '\\App\\Import\\Transformers\\'.$formatted_entity_type . 'Transformer'; - $factoryName = '\\App\\Factory\\'.$formatted_entity_type . 'Factory'; + $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 = new $transformer_name( $this->maps ); - - if ( $this->skip_header ) { - array_shift( $records ); - } + $repository = app()->make( $repository_name ); + $transformer = $this->getTransformer( $entity_type ); foreach ( $records as $record ) { - $keys = $this->column_map[ $entity_type ]; - $values = array_intersect_key( $record, $keys ); + try { + $entity = $transformer->transform( $record ); - $data = array_combine( $keys, $values ); + /** @var \App\Http\Requests\Request $request */ + $request = new $request_name(); - $entity = $transformer->transform( $data ); + // Pass entity data to request so it can be validated + $request->query = $request->request = new ParameterBag( $entity ); + $validator = Validator::make( $entity, $request->rules() ); - $validator = Validator::make( $entity, ( new $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 ) ) ); - if ( $validator->fails() ) { - $this->error_array[ $entity_type ][] = - [ $entity_type => $entity, 'error' => $validator->errors()->all() ]; - } else { - $entity = - $repository->save( $entity, $factoryName::create( $this->company->id, $this->getUserIDForRecord( $data) ) ); + $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'; + } - $entity->save(); - $this->{'add' . $formatted_entity_type . 'ToMaps'}( $entity ); + $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 = [ @@ -313,6 +406,7 @@ class CSVImport implements ShouldQueue { 'invoice_ids' => [], 'vendors' => [], 'expense_categories' => [], + 'payment_types' => [], 'tax_rates' => [], 'tax_names' => [], ]; @@ -337,6 +431,11 @@ class CSVImport implements ShouldQueue { $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; @@ -348,6 +447,11 @@ class CSVImport implements ShouldQueue { $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 ); @@ -370,7 +474,7 @@ class CSVImport implements ShouldQueue { * @param Invoice $invoice */ private function addInvoiceToMaps( Invoice $invoice ) { - if ( $number = strtolower( trim( $invoice->invoice_number ) ) ) { + 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; @@ -391,7 +495,7 @@ class CSVImport implements ShouldQueue { if ( $email = strtolower( trim( $contact->email ) ) ) { $this->maps['client'][ $email ] = $client->id; } - if ( $name = strtolower( trim($contact->first_name.' '.$contact->last_name) ) ) { + 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; @@ -416,6 +520,15 @@ class CSVImport implements ShouldQueue { } } + /** + * @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; } @@ -428,8 +541,8 @@ class CSVImport implements ShouldQueue { private function getUserIDForRecord( $record ) { - if ( !empty($record['client.user_id']) ) { - return $this->findUser( $record[ 'client.user_id' ] ); + if ( ! empty( $record['user_id'] ) ) { + return $this->findUser( $record['user_id'] ); } else { return $this->company->owner()->id; } @@ -473,6 +586,6 @@ class CSVImport implements ShouldQueue { } } - return $data; - } + return $data; + } }