Merge pull request #4943 from turbo124/v5-stable

v5.1.7
This commit is contained in:
David Bomba 2021-02-19 07:59:45 +11:00 committed by GitHub
commit ee00defdc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 108596 additions and 106166 deletions

View File

@ -1 +1 @@
5.1.6 5.1.7

View File

@ -39,10 +39,10 @@ class PaymentWasEmailedAndFailed
* PaymentWasEmailedAndFailed constructor. * PaymentWasEmailedAndFailed constructor.
* @param Payment $payment * @param Payment $payment
* @param $company * @param $company
* @param array $errors * @param string $errors
* @param array $event_vars * @param array $event_vars
*/ */
public function __construct(Payment $payment, Company $company, array $errors, array $event_vars) public function __construct(Payment $payment, Company $company, string $errors, array $event_vars)
{ {
$this->payment = $payment; $this->payment = $payment;

View File

@ -18,11 +18,13 @@ class ContactRegisterController extends Controller
$this->middleware(['guest', 'contact.register']); $this->middleware(['guest', 'contact.register']);
} }
public function showRegisterForm(string $company_key) public function showRegisterForm(string $company_key = '')
{ {
$company = Company::where('company_key', $company_key)->firstOrFail(); $key = request()->has('key') ? request('key') : $company_key;
return render('auth.register', compact(['company'])); $company = Company::where('company_key', $key)->firstOrFail();
return render('auth.register', ['company' => $company]);
} }
public function register(RegisterRequest $request) public function register(RegisterRequest $request)

View File

@ -76,10 +76,10 @@ class DocumentController extends Controller
$options->setSendHttpHeaders(true); $options->setSendHttpHeaders(true);
$zip = new ZipStream('files.zip', $options); $zip = new ZipStream(now() . '-documents.zip', $options);
foreach ($documents as $document) { foreach ($documents as $document) {
$zip->addFileFromPath(basename($document->filePath()), TempFile::path($document->filePath())); $zip->addFileFromPath(basename($document->diskPath()), TempFile::path($document->diskPath()));
} }
$zip->finish(); $zip->finish();

View File

@ -14,19 +14,20 @@ namespace App\Http\Controllers;
use App\Http\Requests\Import\ImportRequest; use App\Http\Requests\Import\ImportRequest;
use App\Http\Requests\Import\PreImportRequest; use App\Http\Requests\Import\PreImportRequest;
use App\Jobs\Import\CSVImport; use App\Jobs\Import\CSVImport;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Csv\Reader; use League\Csv\Reader;
use League\Csv\Statement; use League\Csv\Statement;
class ImportController extends Controller class ImportController extends Controller {
{
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param StoreImportRequest $request * @param PreImportRequest $request
* @return Response *
* @return \Illuminate\Http\JsonResponse
* *
* @OA\Post( * @OA\Post(
* path="/api/v1/preimport", * path="/api/v1/preimport",
@ -69,44 +70,63 @@ class ImportController extends Controller
* ), * ),
* ) * )
*/ */
public function preimport(PreImportRequest $request) public function preimport( PreImportRequest $request ) {
{ // Create a reference
//create a reference $hash = Str::random( 32 );
$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);
//parse CSV
$csv_array = $this->getCsvData(file_get_contents($request->file('file')->getPathname()));
$class_map = $this->getEntityMap($request->input('entity_type'));
$data = [ $data = [
'hash' => $hash, 'hash' => $hash,
'available' => $class_map::importable(), 'mappings' => [],
'headers' => array_slice($csv_array, 0, 2)
]; ];
/** @var UploadedFile $file */
foreach ( $request->files->get( 'files' ) as $entityType => $file ) {
$contents = file_get_contents( $file->getPathname() );
return response()->json($data); // Store the csv in cache with an expiry of 10 minutes
Cache::put( $hash . '-' . $entityType, base64_encode( $contents ), 3600 );
// Parse CSV
$csv_array = $this->getCsvData( $contents );
$class_map = $this->getEntityMap( $entityType );
$data['mappings'][ $entityType ] = [
'available' => $class_map::importable(),
'headers' => array_slice( $csv_array, 0, 2 ),
];
} }
public function import(ImportRequest $request) return response()->json( $data );
{
CSVImport::dispatch($request->all(), auth()->user()->company());
return response()->json(['message' => ctrans('texts.import_started')], 200);
} }
private function getEntityMap($entity_type) public function import( ImportRequest $request ) {
{ $data = $request->all();
return sprintf('App\\Import\\Definitions\%sMap', ucfirst($entity_type));
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 );
}
} }
private function getCsvData($csvfile) CSVImport::dispatch( $data, auth()->user()->company() );
{
if (! ini_get('auto_detect_line_endings')) { return response()->json( [ 'message' => ctrans( 'texts.import_started' ) ], 200 );
ini_set('auto_detect_line_endings', '1'); }
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); $csv = Reader::createFromString($csvfile);
@ -121,9 +141,9 @@ class ImportController extends Controller
$firstCell = $headers[0]; $firstCell = $headers[0];
if (strstr($firstCell, (string)config('ninja.app_name'))) { if (strstr($firstCell, (string)config('ninja.app_name'))) {
array_shift($data); // Invoice Ninja... array_shift( $data ); // Invoice Ninja...
array_shift($data); // <blank line> array_shift( $data ); // <blank line>
array_shift($data); // Enitty Type Header array_shift( $data ); // Entity Type Header
} }
} }
} }

View File

@ -532,7 +532,7 @@ class InvoiceController extends BaseController
} }
}); });
ZipInvoices::dispatch($invoices, $invoices->first()->company, auth()->user()->email); ZipInvoices::dispatch($invoices, $invoices->first()->company, auth()->user());
return response()->json(['message' => ctrans('texts.sent_message')], 200); return response()->json(['message' => ctrans('texts.sent_message')], 200);
} }

View File

@ -14,7 +14,8 @@ namespace App\Http\Controllers;
use App\Console\Commands\ImportMigrations; use App\Console\Commands\ImportMigrations;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use App\Jobs\Mail\MailRouter; use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Util\StartMigration; use App\Jobs\Util\StartMigration;
use App\Mail\ExistingMigration; use App\Mail\ExistingMigration;
use App\Models\Company; use App\Models\Company;
@ -248,7 +249,13 @@ class MigrationController extends BaseController
if ($checks['existing_company'] == true && $checks['force'] == false) { if ($checks['existing_company'] == true && $checks['force'] == false) {
nlog('Migrating: Existing company without force. (CASE_01)'); nlog('Migrating: Existing company without force. (CASE_01)');
MailRouter::dispatch(new ExistingMigration(), $existing_company, $user); $nmo = new NinjaMailerObject;
$nmo->mailable = new ExistingMigration();
$nmo->company = $existing_company;
$nmo->settings = $existing_company->settings;
$nmo->to_user = $user;
NinjaMailerJob::dispatch($nmo);
return response()->json([ return response()->json([
'_id' => Str::uuid(), '_id' => Str::uuid(),

View File

@ -524,7 +524,7 @@ class QuoteController extends BaseController
} }
}); });
ZipInvoices::dispatch($quotes, $quotes->first()->company, auth()->user()->email); ZipInvoices::dispatch($quotes, $quotes->first()->company, auth()->user());
return response()->json(['message' => ctrans('texts.sent_message')], 200); return response()->json(['message' => ctrans('texts.sent_message')], 200);
} }

View File

@ -369,18 +369,21 @@ class UserController extends BaseController
*/ */
public function update(UpdateUserRequest $request, User $user) public function update(UpdateUserRequest $request, User $user)
{ {
$old_email = $user->email;
$old_company_user = $user->company_user; $old_company_user = $user->company_user;
$old_user = $user; $old_user = json_encode($user);
$old_user_email = $user->getOriginal('email');
$new_email = $request->input('email'); $new_email = $request->input('email');
$new_user = $this->user_repo->save($request->all(), $user);
$new_user = $user->fresh();
$user = $this->user_repo->save($request->all(), $user);
$user = $user->fresh();
if ($old_email != $new_email) { nlog($old_user);
UserEmailChanged::dispatch($new_email, $old_email, auth()->user()->company());
} if ($old_user_email != $new_email)
UserEmailChanged::dispatch($new_user, json_decode($old_user), auth()->user()->company());
if( if(
strcasecmp($old_company_user->permissions, $user->company_user->permissions) != 0 || strcasecmp($old_company_user->permissions, $user->company_user->permissions) != 0 ||

View File

@ -25,38 +25,15 @@ class DocumentsTable extends Component
public $per_page = 10; public $per_page = 10;
public $status = [
'resources',
];
public function mount($client) public function mount($client)
{ {
$this->client = $client; $this->client = $client;
} }
public function statusChange($status)
{
if (in_array($status, $this->status)) {
return $this->status = array_diff($this->status, [$status]);
}
array_push($this->status, $status);
}
public function render() public function render()
{ {
$query = $this->client->documents(); $query = $this->client
->documents()
if (in_array('resources', $this->status) && ! in_array('client', $this->status)) {
$query = $query->where('documentable_type', '!=', Client::class);
}
if (in_array('client', $this->status) && ! in_array('resources', $this->status)) {
$query = $query->where('documentable_type', Client::class);
}
$query = $query
->where('is_public', true)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page); ->paginate($this->per_page);

View File

@ -2,7 +2,9 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Models\Account;
use App\Models\Company; use App\Models\Company;
use App\Utils\Ninja;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -17,29 +19,39 @@ class ContactRegister
*/ */
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
/* // Resolving based on subdomain. Used in version 5 hosted platform.
* Notes:
*
* 1. If request supports subdomain (for hosted) check domain and continue request.
* 2. If request doesn't support subdomain and doesn' have company_key, abort
* 3. firstOrFail() will abort with 404 if company with company_key wasn't found.
* 4. Abort if setting isn't enabled.
*/
if ($request->subdomain) { if ($request->subdomain) {
$company = Company::where('subdomain', $request->subdomain)->firstOrFail(); $company = Company::where('subdomain', $request->subdomain)->firstOrFail();
abort_unless($company->getSetting('enable_client_registration'), 404); abort_unless($company->getSetting('enable_client_registration'), 404);
$request->merge(['key' => $company->company_key]);
return $next($request); return $next($request);
} }
abort_unless($request->company_key, 404); // For self-hosted platforms with multiple companies, resolving is done using company key
// if it doesn't resolve using a domain.
if ($request->route()->parameter('company_key') && Ninja::isSelfHost()) {
$company = Company::where('company_key', $request->company_key)->firstOrFail(); $company = Company::where('company_key', $request->company_key)->firstOrFail();
abort_unless($company->client_can_register, 404); abort_unless($company->client_can_register, 404);
return $next($request); return $next($request);
} }
// As a fallback for self-hosted, it will use default company in the system
// if key isn't provided in the url.
if (!$request->route()->parameter('company_key') && Ninja::isSelfHost()) {
$company = Account::first()->default_company;
abort_unless($company->client_can_register, 404);
$request->merge(['key' => $company->company_key]);
return $next($request);
}
return abort(404);
}
} }

View File

@ -27,7 +27,8 @@ class ShowDocumentRequest extends FormRequest
*/ */
public function authorize() public function authorize()
{ {
return auth()->user('contact')->client->id == $this->document->documentable_id; return auth()->user('contact')->client->id == $this->document->documentable_id
|| $this->document->documentable->client_id == auth()->user('contact')->client->id;
} }
/** /**

View File

@ -28,10 +28,12 @@ class ImportRequest extends Request
public function rules() public function rules()
{ {
return [ return [
'hash' => 'required|string', 'import_type' => 'required',
'entity_type' => 'required|string', 'files' => 'required_without:hash|array|min:1|max:6',
'column_map' => 'required|array', 'hash' => 'nullable|string',
'skip_header' => 'required|boolean' 'column_map' => 'required_with:hash|array',
'skip_header' => 'required_with:hash|boolean',
'files.*' => 'file|mimes:csv,txt',
]; ];
} }
} }

View File

@ -28,8 +28,9 @@ class PreImportRequest extends Request
public function rules() public function rules()
{ {
return [ return [
'file' => 'required|file|mimes:csv,txt', 'files.*' => 'file|mimes:csv,txt',
'entity_type' => 'required', 'files' => 'required|array|min:1|max:6',
'import_type' => 'required',
]; ];
} }
} }

View File

@ -57,6 +57,7 @@ class PortalComposer
$data['client'] = auth()->user()->client; $data['client'] = auth()->user()->client;
$data['settings'] = $this->settings; $data['settings'] = $this->settings;
$data['currencies'] = TranslationHelper::getCurrencies(); $data['currencies'] = TranslationHelper::getCurrencies();
$data['contact'] = auth('contact')->user();
$data['multiple_contacts'] = session()->get('multiple_contacts'); $data['multiple_contacts'] = session()->get('multiple_contacts');
@ -69,8 +70,8 @@ class PortalComposer
//@todo wire this back in when we are happy with dashboard. //@todo wire this back in when we are happy with dashboard.
// if($this->settings->enable_client_portal_dashboard == TRUE) // if($this->settings->enable_client_portal_dashboard == TRUE)
// $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
// $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
$data[] = ['title' => ctrans('texts.invoices'), 'url' => 'client.invoices.index', 'icon' => 'file-text']; $data[] = ['title' => ctrans('texts.invoices'), 'url' => 'client.invoices.index', 'icon' => 'file-text'];
$data[] = ['title' => ctrans('texts.recurring_invoices'), 'url' => 'client.recurring_invoices.index', 'icon' => 'file']; $data[] = ['title' => ctrans('texts.recurring_invoices'), 'url' => 'client.recurring_invoices.index', 'icon' => 'file'];
$data[] = ['title' => ctrans('texts.payments'), 'url' => 'client.payments.index', 'icon' => 'credit-card']; $data[] = ['title' => ctrans('texts.payments'), 'url' => 'client.payments.index', 'icon' => 'credit-card'];

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -0,0 +1,51 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Definitions;
class ExpenseMap
{
public static function importable()
{
return [
0 => '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',
];
}
}

View File

@ -26,50 +26,51 @@ class InvoiceMap
7 => 'invoice.date', 7 => 'invoice.date',
8 => 'invoice.due_date', 8 => 'invoice.due_date',
9 => 'invoice.terms', 9 => 'invoice.terms',
10 => 'invoice.public_notes', 10 => 'invoice.status',
11 => 'invoice.is_sent', 11 => 'invoice.public_notes',
12 => 'invoice.private_notes', 12 => 'invoice.is_sent',
13 => 'invoice.uses_inclusive_taxes', 13 => 'invoice.private_notes',
14 => 'invoice.tax_name1', 14 => 'invoice.uses_inclusive_taxes',
15 => 'invoice.tax_rate1', 15 => 'invoice.tax_name1',
16 => 'invoice.tax_name2', 16 => 'invoice.tax_rate1',
17 => 'invoice.tax_rate2', 17 => 'invoice.tax_name2',
18 => 'invoice.tax_name3', 18 => 'invoice.tax_rate2',
19 => 'invoice.tax_rate3', 19 => 'invoice.tax_name3',
20 => 'invoice.is_amount_discount', 20 => 'invoice.tax_rate3',
21 => 'invoice.footer', 21 => 'invoice.is_amount_discount',
22 => 'invoice.partial', 22 => 'invoice.footer',
23 => 'invoice.partial_due_date', 23 => 'invoice.partial',
24 => 'invoice.custom_value1', 24 => 'invoice.partial_due_date',
25 => 'invoice.custom_value2', 25 => 'invoice.custom_value1',
26 => 'invoice.custom_value3', 26 => 'invoice.custom_value2',
27 => 'invoice.custom_value4', 27 => 'invoice.custom_value3',
28 => 'invoice.custom_surcharge1', 28 => 'invoice.custom_value4',
29 => 'invoice.custom_surcharge2', 29 => 'invoice.custom_surcharge1',
30 => 'invoice.custom_surcharge3', 30 => 'invoice.custom_surcharge2',
31 => 'invoice.custom_surcharge4', 31 => 'invoice.custom_surcharge3',
32 => 'invoice.exchange_rate', 32 => 'invoice.custom_surcharge4',
33 => 'payment.date', 33 => 'invoice.exchange_rate',
34 => 'payment.amount', 34 => 'payment.date',
35 => 'payment.transaction_reference', 35 => 'payment.amount',
36 => 'item.quantity', 36 => 'payment.transaction_reference',
37 => 'item.cost', 37 => 'item.quantity',
38 => 'item.product_key', 38 => 'item.cost',
39 => 'item.notes', 39 => 'item.product_key',
40 => 'item.discount', 40 => 'item.notes',
41 => 'item.is_amount_discount', 41 => 'item.discount',
42 => 'item.tax_name1', 42 => 'item.is_amount_discount',
43 => 'item.tax_rate1', 43 => 'item.tax_name1',
44 => 'item.tax_name2', 44 => 'item.tax_rate1',
45 => 'item.tax_rate2', 45 => 'item.tax_name2',
46 => 'item.tax_name3', 46 => 'item.tax_rate2',
47 => 'item.tax_rate3', 47 => 'item.tax_name3',
48 => 'item.custom_value1', 48 => 'item.tax_rate3',
49 => 'item.custom_value2', 49 => 'item.custom_value1',
50 => 'item.custom_value3', 50 => 'item.custom_value2',
51 => 'item.custom_value4', 51 => 'item.custom_value3',
52 => 'item.type_id', 52 => 'item.custom_value4',
53 => 'client.email', 53 => 'item.type_id',
54 => 'client.email',
]; ];
} }
@ -86,50 +87,51 @@ class InvoiceMap
7 => 'texts.date', 7 => 'texts.date',
8 => 'texts.due_date', 8 => 'texts.due_date',
9 => 'texts.terms', 9 => 'texts.terms',
10 => 'texts.public_notes', 10 => 'texts.status',
11 => 'texts.sent', 11 => 'texts.public_notes',
12 => 'texts.private_notes', 12 => 'texts.sent',
13 => 'texts.uses_inclusive_taxes', 13 => 'texts.private_notes',
14 => 'texts.tax_name', 14 => 'texts.uses_inclusive_taxes',
15 => 'texts.tax_rate', 15 => 'texts.tax_name',
16 => 'texts.tax_name', 16 => 'texts.tax_rate',
17 => 'texts.tax_rate', 17 => 'texts.tax_name',
18 => 'texts.tax_name', 18 => 'texts.tax_rate',
19 => 'texts.tax_rate', 19 => 'texts.tax_name',
20 => 'texts.is_amount_discount', 20 => 'texts.tax_rate',
21 => 'texts.footer', 21 => 'texts.is_amount_discount',
22 => 'texts.partial', 22 => 'texts.footer',
23 => 'texts.partial_due_date', 23 => 'texts.partial',
24 => 'texts.custom_value1', 24 => 'texts.partial_due_date',
25 => 'texts.custom_value2', 25 => 'texts.custom_value1',
26 => 'texts.custom_value3', 26 => 'texts.custom_value2',
27 => 'texts.custom_value4', 27 => 'texts.custom_value3',
28 => 'texts.surcharge', 28 => 'texts.custom_value4',
29 => 'texts.surcharge', 29 => 'texts.surcharge',
30 => 'texts.surcharge', 30 => 'texts.surcharge',
31 => 'texts.surcharge', 31 => 'texts.surcharge',
32 => 'texts.exchange_rate', 32 => 'texts.surcharge',
33 => 'texts.payment_date', 33 => 'texts.exchange_rate',
34 => 'texts.payment_amount', 34 => 'texts.payment_date',
35 => 'texts.transaction_reference', 35 => 'texts.payment_amount',
36 => 'texts.quantity', 36 => 'texts.transaction_reference',
37 => 'texts.cost', 37 => 'texts.quantity',
38 => 'texts.product_key', 38 => 'texts.cost',
39 => 'texts.notes', 39 => 'texts.product_key',
40 => 'texts.discount', 40 => 'texts.notes',
41 => 'texts.is_amount_discount', 41 => 'texts.discount',
42 => 'texts.tax_name', 42 => 'texts.is_amount_discount',
43 => 'texts.tax_rate', 43 => 'texts.tax_name',
44 => 'texts.tax_name', 44 => 'texts.tax_rate',
45 => 'texts.tax_rate', 45 => 'texts.tax_name',
46 => 'texts.tax_name', 46 => 'texts.tax_rate',
47 => 'texts.tax_rate', 47 => 'texts.tax_name',
48 => 'texts.custom_value', 48 => 'texts.tax_rate',
49 => 'texts.custom_value', 49 => 'texts.custom_value',
50 => 'texts.custom_value', 50 => 'texts.custom_value',
51 => 'texts.custom_value', 51 => 'texts.custom_value',
52 => 'texts.type', 52 => 'texts.custom_value',
53 => 'texts.email', 53 => 'texts.type',
54 => 'texts.email',
]; ];
} }
} }

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -0,0 +1,61 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Definitions;
class VendorMap
{
public static function importable()
{
return [
0 => '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',
];
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace App\Import;
class ImportException extends \Exception{
}

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */
@ -53,38 +53,29 @@ class BaseTransformer
return (isset($data[$field]) && $data[$field]) ? $data[$field] : '1'; return (isset($data[$field]) && $data[$field]) ? $data[$field] : '1';
} }
public function getCurrencyByCode($data) public function getCurrencyByCode( $data, $key = 'client.currency_id' ) {
{ $code = array_key_exists( $key, $data ) ? $data[ $key ] : false;
$code = array_key_exists('client.currency_id', $data) ? $data['client.currency_id'] : false;
if ($code) { return $this->maps['currencies'][ $code ] ?? $this->maps['company']->settings->currency_id;
$currency = $this->maps['currencies']->where('code', $code)->first();
if ($currency) {
return $currency->id;
}
} }
return $this->maps['company']->settings->currency_id; public function getClient($client_name, $client_email) {
}
public function getClient($client_name, $client_email)
{
$clients = $this->maps['company']->clients; $clients = $this->maps['company']->clients;
$clients = $clients->where('name', $client_name); $clients = $clients->where( 'name', $client_name );
if ($clients->count() >= 1) { if ( $clients->count() >= 1 ) {
return $clients->first()->id; return $clients->first()->id;
} }
if ( ! empty( $client_email ) ) {
$contacts = ClientContact::where( 'company_id', $this->maps['company']->id )
->where( 'email', $client_email );
$contacts = ClientContact::where('company_id', $this->maps['company']->id) if ( $contacts->count() >= 1 ) {
->where('email', $client_email);
if ($contacts->count() >=1) {
return $contacts->first()->client_id; return $contacts->first()->client_id;
} }
}
return null; return null;
} }
@ -101,7 +92,7 @@ class BaseTransformer
{ {
$name = trim(strtolower($name)); $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)); $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)); $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)); $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) 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 = $this->getInvoiceNumber($invoiceNumber);
$invoiceNumber = strtolower($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 = $this->getInvoiceNumber($invoiceNumber);
$invoiceNumber = strtolower($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 = $this->getInvoiceNumber($invoiceNumber);
$invoiceNumber = strtolower($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 = $this->getInvoiceNumber($invoiceNumber);
$invoiceNumber = strtolower($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,7 +377,7 @@ class BaseTransformer
{ {
$name = strtolower(trim($name)); $name = strtolower(trim($name));
return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null; return $this->maps['vendor'][ $name ] ?? null;
} }
/** /**
@ -392,10 +385,31 @@ class BaseTransformer
* *
* @return null * @return null
*/ */
public function getExpenseCategoryId($name) public function getExpenseCategoryId( $name ) {
{ $name = strtolower( trim( $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;
} }
} }

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -0,0 +1,79 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Csv;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use Illuminate\Support\Str;
/**
* Class ClientTransformer.
*/
class ClientTransformer extends BaseTransformer
{
/**
* @param $data
*
* @return array|bool
*/
public function transform($data)
{
if (isset($data->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,
];
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Import\Transformers\Csv;
use App\Import\Transformers\BaseTransformer;
/**
* Class InvoiceTransformer.
*/
class ExpenseTransformer extends BaseTransformer {
/**
* @param $data
*
* @return bool|array
*/
public function transform( $data ) {
$clientId = isset( $data['expense.client'] ) ? $this->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,
];
}
}

View File

@ -0,0 +1,131 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Csv;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use App\Models\Invoice;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer {
/**
* @param $data
*
* @return bool|array
*/
public function transform( $line_items_data ) {
$invoice_data = reset( $line_items_data );
if ( $this->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;
}
}

View File

@ -0,0 +1,64 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Csv;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
/**
* Class PaymentTransformer.
*/
class PaymentTransformer extends BaseTransformer {
/**
* @param $data
*
* @return array
*/
public function transform( $data ) {
$client_id =
$this->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;
}
}

View File

@ -1,16 +1,16 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */
namespace App\Import\Transformers; namespace App\Import\Transformers\Csv;
use App\Import\Transformers\BaseTransformer;
/** /**
* Class ProductTransformer. * Class ProductTransformer.
*/ */
@ -19,7 +19,7 @@ class ProductTransformer extends BaseTransformer
/** /**
* @param $data * @param $data
* *
* @return bool|Item * @return array
*/ */
public function transform($data) public function transform($data)
{ {

View File

@ -0,0 +1,47 @@
<?php
namespace App\Import\Transformers\Csv;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
/**
* Class VendorTransformer.
*/
class VendorTransformer extends BaseTransformer {
/**
* @param $data
*
* @return array|bool
*/
public function transform( $data ) {
if ( isset( $data->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,
];
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* Invoice Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Freshbooks;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use Illuminate\Support\Str;
/**
* Class ClientTransformer.
*/
class ClientTransformer extends BaseTransformer {
/**
* @param $data
*
* @return array|bool
*/
public function transform( $data ) {
if ( isset( $data['Organization'] ) && $this->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' ),
],
],
];
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Freshbooks;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use App\Models\Invoice;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer {
/**
* @param $line_items_data
*
* @return bool|array
*/
public function transform( $line_items_data ) {
$invoice_data = reset( $line_items_data );
if ( $this->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;
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Invoice2Go;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use App\Models\Invoice;
use Illuminate\Support\Str;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer {
/**
* @param $line_items_data
*
* @return bool|array
*/
public function transform( $invoice_data ) {
if ( $this->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;
}
}

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -0,0 +1,48 @@
<?php
/**
* Invoice Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Invoicely;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use Illuminate\Support\Str;
/**
* Class ClientTransformer.
*/
class ClientTransformer extends BaseTransformer {
/**
* @param $data
*
* @return array|bool
*/
public function transform( $data ) {
if ( isset( $data['Client Name'] ) && $this->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' ),
],
],
];
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Invoicely;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use App\Models\Invoice;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer {
/**
* @param $data
*
* @return bool|array
*/
public function transform( $data ) {
if ( $this->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;
}
}

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -0,0 +1,74 @@
<?php
/**
* Invoice Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Waveaccounting;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use Illuminate\Support\Str;
/**
* Class ClientTransformer.
*/
class ClientTransformer extends BaseTransformer {
/**
* @param $data
*
* @return array|bool
*/
public function transform( $data ) {
if ( isset( $data['customer_name'] ) && $this->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' ),
],
],
];
}
}

View File

@ -0,0 +1,80 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Waveaccounting;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use App\Models\Invoice;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer {
/**
* @param $line_items_data
*
* @return bool|array
*/
public function transform( $line_items_data ) {
$invoice_data = reset( $line_items_data );
if ( $this->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;
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* Invoice Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Zoho;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use Illuminate\Support\Str;
/**
* Class ClientTransformer.
*/
class ClientTransformer extends BaseTransformer {
/**
* @param $data
*
* @return array|bool
*/
public function transform( $data ) {
if ( isset( $data['Company Name'] ) && $this->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' ),
],
],
];
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Zoho;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use App\Models\Invoice;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer {
/**
* @param $line_items_data
*
* @return bool|array
*/
public function transform( $line_items_data ) {
$invoice_data = reset( $line_items_data );
if ( $this->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;
}
}

View File

@ -128,14 +128,17 @@ class CreateEntityPdf implements ShouldQueue
$template = new PdfMakerDesign(strtolower($design->name)); $template = new PdfMakerDesign(strtolower($design->name));
} }
$variables = $html->generateLabelsAndValues();
$state = [ $state = [
'template' => $template->elements([ 'template' => $template->elements([
'client' => $this->entity->client, 'client' => $this->entity->client,
'entity' => $this->entity, 'entity' => $this->entity,
'pdf_variables' => (array) $this->entity->company->settings->pdf_variables, 'pdf_variables' => (array) $this->entity->company->settings->pdf_variables,
'$product' => $design->design->product, '$product' => $design->design->product,
'variables' => $variables,
]), ]),
'variables' => $html->generateLabelsAndValues(), 'variables' => $variables,
'options' => [ 'options' => [
'all_pages_header' => $this->entity->client->getSetting('all_pages_header'), 'all_pages_header' => $this->entity->client->getSetting('all_pages_header'),
'all_pages_footer' => $this->entity->client->getSetting('all_pages_footer'), 'all_pages_footer' => $this->entity->client->getSetting('all_pages_footer'),

View File

@ -14,7 +14,6 @@ namespace App\Jobs\Entity;
use App\Events\Invoice\InvoiceReminderWasEmailed; use App\Events\Invoice\InvoiceReminderWasEmailed;
use App\Events\Invoice\InvoiceWasEmailed; use App\Events\Invoice\InvoiceWasEmailed;
use App\Events\Invoice\InvoiceWasEmailedAndFailed; use App\Events\Invoice\InvoiceWasEmailedAndFailed;
use App\Jobs\Mail\BaseMailerJob;
use App\Jobs\Mail\EntityFailedSendMailer; use App\Jobs\Mail\EntityFailedSendMailer;
use App\Jobs\Mail\NinjaMailerJob; use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject; use App\Jobs\Mail\NinjaMailerObject;
@ -113,6 +112,7 @@ class EmailEntity implements ShouldQueue
$nmo->entity_string = $this->entity_string; $nmo->entity_string = $this->entity_string;
$nmo->invitation = $this->invitation; $nmo->invitation = $this->invitation;
$nmo->reminder_template = $this->reminder_template; $nmo->reminder_template = $this->reminder_template;
$nmo->entity = $this->entity;
NinjaMailerJob::dispatch($nmo); NinjaMailerJob::dispatch($nmo);

View File

@ -13,26 +13,31 @@ namespace App\Jobs\Import;
use App\Factory\ClientFactory; use App\Factory\ClientFactory;
use App\Factory\InvoiceFactory; use App\Factory\InvoiceFactory;
use App\Factory\ProductFactory; use App\Factory\PaymentFactory;
use App\Http\Requests\Client\StoreClientRequest;
use App\Http\Requests\Invoice\StoreInvoiceRequest; use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Product\StoreProductRequest; use App\Import\ImportException;
use App\Import\Transformers\ClientTransformer; use App\Import\Transformers\BaseTransformer;
use App\Import\Transformers\InvoiceItemTransformer; use App\Jobs\Mail\NinjaMailerJob;
use App\Import\Transformers\InvoiceTransformer; use App\Jobs\Mail\NinjaMailerObject;
use App\Import\Transformers\ProductTransformer;
use App\Jobs\Mail\MailRouter;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Mail\Import\ImportCompleted; use App\Mail\Import\ImportCompleted;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company; use App\Models\Company;
use App\Models\Country;
use App\Models\Currency; use App\Models\Currency;
use App\Models\ExpenseCategory;
use App\Models\Invoice; 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\Models\User;
use App\Repositories\ClientContactRepository; use App\Models\Vendor;
use App\Repositories\BaseRepository;
use App\Repositories\ClientRepository; use App\Repositories\ClientRepository;
use App\Repositories\InvoiceRepository; use App\Repositories\InvoiceRepository;
use App\Repositories\ProductRepository; use App\Repositories\PaymentRepository;
use App\Utils\Traits\CleanLineItems; use App\Utils\Traits\CleanLineItems;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@ -42,11 +47,13 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use League\Csv\Reader; use League\Csv\Reader;
use League\Csv\Statement; use League\Csv\Statement;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
class CSVImport implements ShouldQueue class CSVImport implements ShouldQueue {
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, CleanLineItems; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, CleanLineItems;
public $invoice; public $invoice;
@ -55,7 +62,7 @@ class CSVImport implements ShouldQueue
public $hash; public $hash;
public $entity_type; public $import_type;
public $skip_header; public $skip_header;
@ -63,21 +70,16 @@ class CSVImport implements ShouldQueue
public $import_array; public $import_array;
public $error_array; public $error_array = [];
public $maps; public $maps;
public function __construct(array $request, Company $company) public function __construct( array $request, Company $company ) {
{
$this->company = $company; $this->company = $company;
$this->hash = $request['hash']; $this->hash = $request['hash'];
$this->import_type = $request['import_type'];
$this->entity_type = $request['entity_type']; $this->skip_header = $request['skip_header'] ?? null;
$this->column_map = $request['column_map'] ?? null;
$this->skip_header = $request['skip_header'];
$this->column_map = $request['column_map'];
} }
/** /**
@ -86,281 +88,517 @@ class CSVImport implements ShouldQueue
* *
* @return void * @return void
*/ */
public function handle() public function handle() {
{
MultiDB::setDb($this->company->db);
$this->company->owner()->setCompany($this->company); MultiDB::setDb( $this->company->db );
Auth::login($this->company->owner(), true);
Auth::login( $this->company->owner(), true );
$this->company->owner()->setCompany( $this->company );
$this->buildMaps(); $this->buildMaps();
//sort the array by key nlog( "import " . $this->import_type );
ksort($this->column_map); foreach ( [ 'client', 'product', 'invoice', 'payment', 'vendor', 'expense' ] as $entityType ) {
$csvData = $this->getCsvData( $entityType );
nlog("import".ucfirst($this->entity_type)); if ( ! empty( $csvData ) ) {
$this->{"import".ucfirst($this->entity_type)}(); $importFunction = "import" . Str::plural( Str::title( $entityType ) );
$preTransformFunction = "preTransform" . Str::title( $this->import_type );
$data = [ if ( method_exists( $this, $preTransformFunction ) ) {
'entity' => ucfirst($this->entity_type), $csvData = $this->$preTransformFunction( $csvData, $entityType );
'errors' => $this->error_array,
'clients' => $this->maps['clients'],
'products' => $this->maps['products'],
'invoices' => $this->maps['invoices'],
'settings' => $this->company->settings
];
//nlog(print_r($data, 1));
MailRouter::dispatch(new ImportCompleted($data), $this->company, auth()->user());
} }
public function failed($exception) if ( empty( $csvData ) ) {
{ continue;
}
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 );
}
}
}
$data = [
'errors' => $this->error_array,
'company' => $this->company,
];
$nmo = new NinjaMailerObject;
$nmo->mailable = new ImportCompleted( $data );
$nmo->company = $this->company;
$nmo->settings = $this->company->settings;
$nmo->to_user = $this->company->owner();
NinjaMailerJob::dispatch($nmo);
} }
////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////
private function preTransformCsv( $csvData, $entityType ) {
if ( empty( $this->column_map[ $entityType ] ) ) {
return false;
private function importInvoice()
{
$invoice_transformer = new InvoiceTransformer($this->maps);
$records = $this->getCsvData();
$invoice_number_key = array_search('Invoice Number', reset($records));
if ($this->skip_header) {
array_shift($records);
} }
if (!$invoice_number_key) { if ( $this->skip_header ) {
nlog("no invoice number to use as key - returning"); array_shift( $csvData );
return;
} }
$unique_invoices = []; //sort the array by key
$keys = $this->column_map[ $entityType ];
ksort( $keys );
//get an array of unique invoice numbers $csvData = array_map( function ( $row ) use ( $keys ) {
foreach ($records as $key => $value) { return array_combine( $keys, array_intersect_key( $row, $keys ) );
$unique_invoices[] = $value[$invoice_number_key]; }, $csvData );
if ( $entityType === 'invoice' ) {
$csvData = $this->groupInvoices( $csvData, 'invoice.number' );
} }
foreach ($unique_invoices as $unique) { return $csvData;
$invoices = array_filter($records, function ($value) use ($invoice_number_key, $unique) { }
return $value[$invoice_number_key] == $unique;
});
$keys = $this->column_map; private function preTransformFreshbooks( $csvData, $entityType ) {
$values = array_intersect_key(reset($invoices), $this->column_map); $csvData = $this->mapCSVHeaderToKeys( $csvData );
$invoice_data = array_combine($keys, $values);
$invoice = $invoice_transformer->transform($invoice_data); if ( $entityType === 'invoice' ) {
$csvData = $this->groupInvoices( $csvData, 'Invoice #' );
}
$this->processInvoice($invoices, $invoice); 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;
} }
} }
private function processInvoice($invoices, $invoice) 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 );
$payment_repository->import_mode = true;
/** @var ClientRepository $client_repository */
$client_repository = app()->make( ClientRepository::class );
$client_repository->import_mode = true;
$invoice_repository = new InvoiceRepository(); $invoice_repository = new InvoiceRepository();
$item_transformer = new InvoiceItemTransformer($this->maps); $invoice_repository->import_mode = true;
$items = [];
foreach ($invoices as $record) { foreach ( $invoices as $raw_invoice ) {
$keys = $this->column_map; try {
$values = array_intersect_key($record, $this->column_map); $invoice_data = $invoice_transformer->transform( $raw_invoice );
$invoice_data = array_combine($keys, $values);
$items[] = $item_transformer->transform($invoice_data); $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'] );
} }
$invoice['line_items'] = $this->cleanItems($items); $validator = Validator::make( $invoice_data, ( new StoreInvoiceRequest() )->rules() );
if ( $validator->fails() ) {
$validator = Validator::make($invoice, (new StoreInvoiceRequest())->rules()); $this->error_array['invoice'][] =
[ 'invoice' => $invoice_data, 'error' => $validator->errors()->all() ];
if ($validator->fails()) {
$this->error_array['invoices'] = ['invoice' => $invoice, 'error' => json_encode($validator->errors())];
} else { } else {
if ($validator->fails()) { $invoice = InvoiceFactory::create( $this->company->id, $this->getUserIDForRecord( $invoice_data ) );
$this->error_array[] = ['invoice' => $invoice, 'error' => json_encode($validator->errors())]; 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 { } else {
$invoice = $invoice_repository->save($invoice, InvoiceFactory::create($this->company->id, $this->setUser($record))); report( $ex );
$message = 'Unknown error';
}
$this->maps['invoices'][] = $invoice->id; $this->error_array['invoice'][] = [ 'invoice' => $raw_invoice, 'error' => $message ];
$this->performInvoiceActions($invoice, $record, $invoice_repository);
} }
} }
} }
private function performInvoiceActions($invoice, $record, $invoice_repository) private function actionInvoiceStatus( $invoice, $invoice_data, $invoice_repository ) {
{ if ( ! empty( $invoice_data['archived'] ) ) {
$invoice = $this->actionInvoiceStatus($invoice, $record, $invoice_repository); $invoice_repository->archive( $invoice );
}
private function actionInvoiceStatus($invoice, $status, $invoice_repository)
{
switch ($status) {
case 'Archived':
$invoice_repository->archive($invoice);
$invoice->fresh(); $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) { 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->status_id = Invoice::STATUS_PARTIAL;
$invoice->save(); $invoice->save();
} elseif ( $invoice->balance <= 0 ) {
$invoice->status_id = Invoice::STATUS_PAID;
$invoice->save();
} }
}
return $invoice; return $invoice;
} }
//todo limit client imports for hosted version private function importEntities( $records, $entity_type ) {
private function importClient() $entity_type = Str::slug( $entity_type, '_' );
{ $formatted_entity_type = Str::title( $entity_type );
//clients
$records = $this->getCsvData();
$contact_repository = new ClientContactRepository(); $request_name = "\\App\\Http\\Requests\\${formatted_entity_type}\\Store${formatted_entity_type}Request";
$client_repository = new ClientRepository($contact_repository); $repository_name = '\\App\\Repositories\\' . $formatted_entity_type . 'Repository';
$client_transformer = new ClientTransformer($this->maps); $factoryName = '\\App\\Factory\\' . $formatted_entity_type . 'Factory';
if ($this->skip_header) { /** @var BaseRepository $repository */
array_shift($records); $repository = app()->make( $repository_name );
} $repository->import_mode = true;
foreach ($records as $record) { $transformer = $this->getTransformer( $entity_type );
$keys = $this->column_map;
$values = array_intersect_key($record, $this->column_map);
$client_data = array_combine($keys, $values); foreach ( $records as $record ) {
try {
$entity = $transformer->transform( $record );
$client = $client_transformer->transform($client_data); /** @var \App\Http\Requests\Request $request */
$request = new $request_name();
$validator = Validator::make($client, (new StoreClientRequest())->rules()); // 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()) { if ( $validator->fails() ) {
$this->error_array['clients'] = ['client' => $client, 'error' => json_encode($validator->errors())]; $this->error_array[ $entity_type ][] =
[ $entity_type => $record, 'error' => $validator->errors()->all() ];
} else { } else {
$client = $client_repository->save($client, ClientFactory::create($this->company->id, $this->setUser($record))); $entity =
$repository->save(
array_diff_key( $entity, [ 'user_id' => false ] ),
$factoryName::create( $this->company->id, $this->getUserIDForRecord( $entity ) ) );
if (array_key_exists('client.balance', $client_data)) { $entity->save();
$client->balance = preg_replace('/[^0-9,.]+/', '', $client_data['client.balance']); if ( method_exists( $this, 'add' . $formatted_entity_type . 'ToMaps' ) ) {
} $this->{'add' . $formatted_entity_type . 'ToMaps'}( $entity );
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;
} }
} }
} } catch ( \Exception $ex ) {
if ( $ex instanceof ImportException ) {
$message = $ex->getMessage();
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 { } else {
$product = $product_repository->save($product, ProductFactory::create($this->company->id, $this->setUser($record))); report( $ex );
$message = 'Unknown error';
}
$product->save(); $this->error_array[ $entity_type ][] = [ $entity_type => $record, 'error' => $message ];
$this->maps['products'][] = $product->id;
} }
} }
} }
/**
* @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() private function buildMaps() {
{ $this->maps = [
$this->maps['currencies'] = Currency::all(); 'company' => $this->company,
$this->maps['users'] = $this->company->users; 'client' => [],
$this->maps['company'] = $this->company; 'contact' => [],
$this->maps['clients'] = []; 'invoice' => [],
$this->maps['products'] = []; 'invoice_client' => [],
$this->maps['invoices'] = []; 'product' => [],
'countries' => [],
'countries2' => [],
'currencies' => [],
'client_ids' => [],
'invoice_ids' => [],
'vendors' => [],
'expense_categories' => [],
'payment_types' => [],
'tax_rates' => [],
'tax_names' => [],
];
return $this; $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( $project );
}
$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 setUser($record) private function getUserIDForRecord( $record ) {
{ if ( ! empty( $record['user_id'] ) ) {
$user_key_exists = array_search('client.user_id', $this->column_map); return $this->findUser( $record['user_id'] );
if ($user_key_exists) {
return $this->findUser($record[$user_key_exists]);
} else { } else {
return $this->company->owner()->id; return $this->company->owner()->id;
} }
} }
private function findUser($user_hash) private function findUser( $user_hash ) {
{ $user = User::where( 'company_id', $this->company->id )
$user = User::where('company_id', $this->company->id) ->where( \DB::raw( 'CONCAT_WS(" ", first_name, last_name)' ), 'like', '%' . $user_hash . '%' )
->where(\DB::raw('CONCAT_WS(" ", first_name, last_name)'), 'like', '%' . $user_hash . '%')
->first(); ->first();
if ($user) { if ( $user ) {
return $user->id; return $user->id;
} else { } else {
return $this->company->owner()->id; return $this->company->owner()->id;
} }
} }
private function getCsvData() private function getCsvData( $entityType ) {
{ $base64_encoded_csv = Cache::get( $this->hash . '-' . $entityType );
$base64_encoded_csv = Cache::get($this->hash); if ( empty( $base64_encoded_csv ) ) {
$csv = base64_decode($base64_encoded_csv); return null;
$csv = Reader::createFromString($csv); }
$csv = base64_decode( $base64_encoded_csv );
$csv = Reader::createFromString( $csv );
$stmt = new Statement(); $stmt = new Statement();
$data = iterator_to_array($stmt->process($csv)); $data = iterator_to_array( $stmt->process( $csv ) );
if (count($data) > 0) { if ( count( $data ) > 0 ) {
$headers = $data[0]; $headers = $data[0];
// Remove Invoice Ninja headers // Remove Invoice Ninja headers
if (count($headers) && count($data) > 4) { if ( count( $headers ) && count( $data ) > 4 && $this->import_type === 'csv' ) {
$firstCell = $headers[0]; $firstCell = $headers[0];
if (strstr($firstCell, config('ninja.app_name'))) { if ( strstr( $firstCell, config( 'ninja.app_name' ) ) ) {
array_shift($data); // Invoice Ninja... array_shift( $data ); // Invoice Ninja...
array_shift($data); // <blank line> array_shift( $data ); // <blank line>
array_shift($data); // Enitty Type Header array_shift( $data ); // Enitty Type Header
} }
} }
} }

View File

@ -11,10 +11,12 @@
namespace App\Jobs\Invoice; namespace App\Jobs\Invoice;
use App\Jobs\Mail\BaseMailerJob; use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Util\UnlinkFile; use App\Jobs\Util\UnlinkFile;
use App\Mail\DownloadInvoices; use App\Mail\DownloadInvoices;
use App\Models\Company; use App\Models\Company;
use App\Models\User;
use App\Utils\TempFile; use App\Utils\TempFile;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@ -26,7 +28,7 @@ use Illuminate\Support\Facades\Storage;
use ZipStream\Option\Archive; use ZipStream\Option\Archive;
use ZipStream\ZipStream; use ZipStream\ZipStream;
class ZipInvoices extends BaseMailerJob implements ShouldQueue class ZipInvoices implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@ -34,7 +36,7 @@ class ZipInvoices extends BaseMailerJob implements ShouldQueue
private $company; private $company;
private $email; private $user;
public $settings; public $settings;
@ -46,13 +48,13 @@ class ZipInvoices extends BaseMailerJob implements ShouldQueue
* Create a new job instance. * Create a new job instance.
* *
*/ */
public function __construct($invoices, Company $company, $email) public function __construct($invoices, Company $company, User $user)
{ {
$this->invoices = $invoices; $this->invoices = $invoices;
$this->company = $company; $this->company = $company;
$this->email = $email; $this->user = $user;
$this->settings = $company->settings; $this->settings = $company->settings;
} }
@ -90,14 +92,13 @@ class ZipInvoices extends BaseMailerJob implements ShouldQueue
fclose($tempStream); fclose($tempStream);
$this->setMailDriver(); $nmo = new NinjaMailerObject;
$nmo->mailable = new DownloadInvoices(Storage::disk(config('filesystems.default'))->url($path.$file_name), $this->company);
$nmo->to_user = $this->user;
$nmo->settings = $this->settings;
$nmo->company = $this->company;
try { NinjaMailerJob::dispatch($nmo);
Mail::to($this->email)
->send(new DownloadInvoices(Storage::disk(config('filesystems.default'))->url($path.$file_name), $this->company));
} catch (\Exception $e) {
// //$this->failed($e);
}
UnlinkFile::dispatch(config('filesystems.default'), $path.$file_name)->delay(now()->addHours(1)); UnlinkFile::dispatch(config('filesystems.default'), $path.$file_name)->delay(now()->addHours(1));
} }

View File

@ -1,120 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Jobs\Mail;
use App\DataMapper\Analytics\EmailFailure;
use App\Jobs\Util\SystemLogger;
use App\Libraries\Google\Google;
use App\Models\SystemLog;
use App\Models\User;
use App\Providers\MailServiceProvider;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Lang;
use Turbo124\Beacon\Facades\LightLogs;
/*
Multi Mailer implemented
@Deprecated 14/02/2021
*/
class BaseMailerJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
public $tries = 5; //number of retries
public $backoff = 5; //seconds to wait until retry
public $deleteWhenMissingModels = true;
public function setMailDriver()
{
/* Singletons need to be rebooted each time just in case our Locale is changing*/
App::forgetInstance('translator');
App::forgetInstance('mail.manager'); //singletons must be destroyed!
/* Inject custom translations if any exist */
Lang::replace(Ninja::transformTranslations($this->settings));
switch ($this->settings->email_sending_method) {
case 'default':
break;
case 'gmail':
$this->setGmailMailer();
break;
default:
break;
}
}
public function setGmailMailer()
{
$sending_user = $this->settings->gmail_sending_user_id;
$user = User::find($this->decodePrimaryKey($sending_user));
$google = (new Google())->init();
$google->getClient()->setAccessToken(json_encode($user->oauth_user_token));
if ($google->getClient()->isAccessTokenExpired()) {
$google->refreshToken($user);
}
/*
* Now that our token is refreshed and valid we can boot the
* mail driver at runtime and also set the token which will persist
* just for this request.
*/
// config(['mail.driver' => 'gmail']);
// config(['services.gmail.token' => $user->oauth_user_token->access_token]);
// config(['mail.from.address' => $user->email]);
// config(['mail.from.name' => $user->present()->name()]);
//(new MailServiceProvider(app()))->register();
nlog("after registering mail service provider");
nlog(config('services.gmail.token'));
}
public function logMailError($errors, $recipient_object)
{
SystemLogger::dispatch(
$errors,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_SEND,
SystemLog::TYPE_FAILURE,
$recipient_object
);
}
public function failed($exception = null)
{
nlog('mailer job failed');
nlog($exception->getMessage());
$job_failure = new EmailFailure();
$job_failure->string_metric5 = get_parent_class($this);
$job_failure->string_metric6 = $exception->getMessage();
LightLogs::create($job_failure)
->batch();
}
}

View File

@ -1,83 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Jobs\Mail;
use App\Libraries\MultiDB;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
/*Multi Mailer Router implemented*/
class MailRouter extends BaseMailerJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $mailable;
public $company;
public $to_user; //User or ClientContact
public $sending_method; //not sure if we even need this
public $settings;
public function __construct(Mailable $mailable, Company $company, $to_user, $sending_method = null)
{
$this->mailable = $mailable;
$this->company = $company;
$this->to_user = $to_user;
$this->sending_method = $sending_method;
if ($to_user instanceof ClientContact) {
$this->settings = $to_user->client->getMergedSettings();
} else {
$this->settings = $this->company->settings;
}
}
public function handle()
{
/*If we are migrating data we don't want to fire these notification*/
if ($this->company->is_disabled) {
return true;
}
MultiDB::setDb($this->company->db);
//if we need to set an email driver do it now
$this->setMailDriver();
//send email
try {
Mail::to($this->to_user->email)
->send($this->mailable);
} catch (\Exception $e) {
//$this->failed($e);
if ($this->to_user instanceof ClientContact) {
$this->logMailError($e->getMessage(), $this->to_user->client);
}
}
}
}

View File

@ -13,17 +13,21 @@ namespace App\Jobs\Mail;
use App\DataMapper\Analytics\EmailFailure; use App\DataMapper\Analytics\EmailFailure;
use App\Events\Invoice\InvoiceWasEmailedAndFailed; use App\Events\Invoice\InvoiceWasEmailedAndFailed;
use App\Events\Payment\PaymentWasEmailedAndFailed;
use App\Jobs\Mail\NinjaMailerObject; use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Util\SystemLogger; use App\Jobs\Util\SystemLogger;
use App\Libraries\Google\Google; use App\Libraries\Google\Google;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Mail\TemplateEmail; use App\Mail\TemplateEmail;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\Models\User; use App\Models\User;
use App\Providers\MailServiceProvider; use App\Providers\MailServiceProvider;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Dacastro4\LaravelGmail\Facade\LaravelGmail;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@ -34,7 +38,6 @@ use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Lang; use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Turbo124\Beacon\Facades\LightLogs; use Turbo124\Beacon\Facades\LightLogs;
use Dacastro4\LaravelGmail\Facade\LaravelGmail;
/*Multi Mailer implemented*/ /*Multi Mailer implemented*/
@ -77,11 +80,9 @@ class NinjaMailerJob implements ShouldQueue
} catch (\Exception $e) { } catch (\Exception $e) {
nlog("error failed with {$e->getMessage()}"); nlog("error failed with {$e->getMessage()}");
nlog($e);
if ($this->nmo->to_user instanceof ClientContact) if($this->nmo->entity)
$this->logMailError($e->getMessage(), $this->nmo->to_user->client);
if($this->nmo->entity_string)
$this->entityEmailFailed($e->getMessage()); $this->entityEmailFailed($e->getMessage());
} }
} }
@ -89,15 +90,22 @@ class NinjaMailerJob implements ShouldQueue
/* Switch statement to handle failure notifications */ /* Switch statement to handle failure notifications */
private function entityEmailFailed($message) private function entityEmailFailed($message)
{ {
switch ($this->nmo->entity_string) { $class = get_class($this->nmo->entity);
case 'invoice':
switch ($class) {
case Invoice::class:
event(new InvoiceWasEmailedAndFailed($this->nmo->invitation, $this->nmo->company, $message, $this->nmo->reminder_template, Ninja::eventVars())); event(new InvoiceWasEmailedAndFailed($this->nmo->invitation, $this->nmo->company, $message, $this->nmo->reminder_template, Ninja::eventVars()));
break; break;
case Payment::class:
event(new PaymentWasEmailedAndFailed($this->nmo->entity, $this->nmo->company, $message, Ninja::eventVars()));
break;
default: default:
# code... # code...
break; break;
} }
if ($this->nmo->to_user instanceof ClientContact)
$this->logMailError($message, $this->nmo->to_user->client);
} }
private function setMailDriver() private function setMailDriver()
@ -113,7 +121,7 @@ class NinjaMailerJob implements ShouldQueue
switch ($this->nmo->settings->email_sending_method) { switch ($this->nmo->settings->email_sending_method) {
case 'default': case 'default':
config(['mail.driver' => config('mail.default')]); //config(['mail.driver' => config('mail.default')]);
break; break;
case 'gmail': case 'gmail':
$this->setGmailMailer(); $this->setGmailMailer();

View File

@ -35,4 +35,7 @@ class NinjaMailerObject
public $invitation = FALSE; public $invitation = FALSE;
public $template = FALSE; public $template = FALSE;
public $entity = FALSE;
} }

View File

@ -28,7 +28,7 @@ use Illuminate\Support\Facades\Mail;
/*Multi Mailer implemented*/ /*Multi Mailer implemented*/
class PaymentFailureMailer extends BaseMailerJob implements ShouldQueue class PaymentFailureMailer implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, UserNotifies; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, UserNotifies;

View File

@ -13,7 +13,8 @@ namespace App\Jobs\Payment;
use App\Events\Payment\PaymentWasEmailed; use App\Events\Payment\PaymentWasEmailed;
use App\Events\Payment\PaymentWasEmailedAndFailed; use App\Events\Payment\PaymentWasEmailedAndFailed;
use App\Jobs\Mail\BaseMailerJob; use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Mail\Engine\PaymentEmailEngine; use App\Mail\Engine\PaymentEmailEngine;
use App\Mail\TemplateEmail; use App\Mail\TemplateEmail;
@ -28,7 +29,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
class EmailPayment extends BaseMailerJob implements ShouldQueue class EmailPayment implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@ -66,27 +67,24 @@ class EmailPayment extends BaseMailerJob implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
if ($this->company->is_disabled) { if ($this->company->is_disabled)
return true; return true;
}
if ($this->contact->email) { if ($this->contact->email) {
MultiDB::setDb($this->company->db);
//if we need to set an email driver do it now MultiDB::setDb($this->company->db);
$this->setMailDriver();
$email_builder = (new PaymentEmailEngine($this->payment, $this->contact))->build(); $email_builder = (new PaymentEmailEngine($this->payment, $this->contact))->build();
try { $nmo = new NinjaMailerObject;
$mail = Mail::to($this->contact->email, $this->contact->present()->name()); $nmo->mailable = new TemplateEmail($email_builder, $this->contact);
$mail->send(new TemplateEmail($email_builder, $this->contact)); $nmo->to_user = $this->contact;
} catch (\Exception $e) { $nmo->settings = $this->settings;
nlog("mailing failed with message " . $e->getMessage()); $nmo->company = $this->company;
event(new PaymentWasEmailedAndFailed($this->payment, $this->company, Mail::failures(), Ninja::eventVars())); $nmo->entity = $this->payment;
//$this->failed($e);
return $this->logMailError($e->getMessage(), $this->payment->client); NinjaMailerJob::dispatch($nmo);
}
event(new PaymentWasEmailed($this->payment, $this->payment->company, Ninja::eventVars())); event(new PaymentWasEmailed($this->payment, $this->payment->company, Ninja::eventVars()));
} }

View File

@ -11,10 +11,12 @@
namespace App\Jobs\User; namespace App\Jobs\User;
use App\Jobs\Mail\BaseMailerJob; use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Mail\User\UserNotificationMailer; use App\Mail\User\UserNotificationMailer;
use App\Models\Company; use App\Models\Company;
use App\Models\User;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@ -23,13 +25,13 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use stdClass; use stdClass;
class UserEmailChanged extends BaseMailerJob implements ShouldQueue class UserEmailChanged implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $new_email; protected $new_user;
protected $old_email; protected $old_user;
protected $company; protected $company;
@ -42,26 +44,24 @@ class UserEmailChanged extends BaseMailerJob implements ShouldQueue
* @param string $old_email * @param string $old_email
* @param Company $company * @param Company $company
*/ */
public function __construct(string $new_email, string $old_email, Company $company) public function __construct(User $new_user, $old_user, Company $company)
{ {
$this->new_email = $new_email; $this->new_user = $new_user;
$this->old_email = $old_email; $this->old_user = $old_user;
$this->company = $company; $this->company = $company;
$this->settings = $this->company->settings; $this->settings = $this->company->settings;
} }
public function handle() public function handle()
{ {
if ($this->company->is_disabled) { nlog("notifying user of email change");
if ($this->company->is_disabled)
return true; return true;
}
//Set DB //Set DB
MultiDB::setDb($this->company->db); MultiDB::setDb($this->company->db);
//If we need to set an email driver do it now
$this->setMailDriver();
/*Build the object*/ /*Build the object*/
$mail_obj = new stdClass; $mail_obj = new stdClass;
$mail_obj->subject = ctrans('texts.email_address_changed'); $mail_obj->subject = ctrans('texts.email_address_changed');
@ -71,17 +71,19 @@ class UserEmailChanged extends BaseMailerJob implements ShouldQueue
$mail_obj->data = $this->getData(); $mail_obj->data = $this->getData();
//Send email via a Mailable class //Send email via a Mailable class
//
try {
Mail::to($this->old_email)
->send(new UserNotificationMailer($mail_obj));
Mail::to($this->new_email) $nmo = new NinjaMailerObject;
->send(new UserNotificationMailer($mail_obj)); $nmo->mailable = new UserNotificationMailer($mail_obj);
} catch (\Exception $e) { $nmo->settings = $this->settings;
//$this->failed($e); $nmo->company = $this->company;
$this->logMailError($e->getMessage(), $this->company->owner()); $nmo->to_user = $this->old_user;
}
NinjaMailerJob::dispatch($nmo);
$nmo->to_user = $this->new_user;
NinjaMailerJob::dispatch($nmo);
} }
private function getData() private function getData()
@ -90,8 +92,8 @@ class UserEmailChanged extends BaseMailerJob implements ShouldQueue
'title' => ctrans('texts.email_address_changed'), 'title' => ctrans('texts.email_address_changed'),
'message' => ctrans( 'message' => ctrans(
'texts.email_address_changed_message', 'texts.email_address_changed_message',
['old_email' => $this->old_email, ['old_email' => $this->old_user->email,
'new_email' => $this->new_email, 'new_email' => $this->new_user->email,
] ]
), ),
'url' => config('ninja.app_url'), 'url' => config('ninja.app_url'),

View File

@ -52,7 +52,7 @@ class CreditEmailedNotification implements ShouldQueue
foreach ($event->invitation->company->company_users as $company_user) { foreach ($event->invitation->company->company_users as $company_user) {
$user = $company_user->user; $user = $company_user->user;
$notification = new EntitySentNotification($event->invitation, 'credit'); // $notification = new EntitySentNotification($event->invitation, 'credit');
$methods = $this->findUserNotificationTypes($event->invitation, $company_user, 'credit', ['all_notifications', 'credit_sent']); $methods = $this->findUserNotificationTypes($event->invitation, $company_user, 'credit', ['all_notifications', 'credit_sent']);
@ -66,9 +66,9 @@ class CreditEmailedNotification implements ShouldQueue
$first_notification_sent = false; $first_notification_sent = false;
} }
$notification->method = $methods; // $notification->method = $methods;
$user->notify($notification); // $user->notify($notification);
} }
} }
} }

View File

@ -57,7 +57,7 @@ class InvoiceEmailedNotification implements ShouldQueue
$user = $company_user->user; $user = $company_user->user;
/* This is only here to handle the alternate message channels - ie Slack */ /* This is only here to handle the alternate message channels - ie Slack */
$notification = new EntitySentNotification($event->invitation, 'invoice'); // $notification = new EntitySentNotification($event->invitation, 'invoice');
/* Returns an array of notification methods */ /* Returns an array of notification methods */
$methods = $this->findUserNotificationTypes($event->invitation, $company_user, 'invoice', ['all_notifications', 'invoice_sent']); $methods = $this->findUserNotificationTypes($event->invitation, $company_user, 'invoice', ['all_notifications', 'invoice_sent']);
@ -76,10 +76,10 @@ class InvoiceEmailedNotification implements ShouldQueue
} }
/* Override the methods in the Notification Class */ /* Override the methods in the Notification Class */
$notification->method = $methods; // $notification->method = $methods;
/* Notify on the alternate channels */ // Notify on the alternate channels
$user->notify($notification); // $user->notify($notification);
} }
} }
} }

View File

@ -20,7 +20,7 @@ use App\Notifications\Admin\EntitySentNotification;
use App\Utils\Traits\Notifications\UserNotifies; use App\Utils\Traits\Notifications\UserNotifies;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
class InvoiceFailedEmailNotification implements ShouldQueue class InvoiceFailedEmailNotification
{ {
use UserNotifies; use UserNotifies;
@ -54,7 +54,7 @@ class InvoiceFailedEmailNotification implements ShouldQueue
foreach ($event->invitation->company->company_users as $company_user) { foreach ($event->invitation->company->company_users as $company_user) {
$user = $company_user->user; $user = $company_user->user;
$notification = new EntitySentNotification($event->invitation, 'invoice'); // $notification = new EntitySentNotification($event->invitation, 'invoice');
$methods = $this->findUserNotificationTypes($event->invitation, $company_user, 'invoice', ['all_notifications', 'invoice_sent']); $methods = $this->findUserNotificationTypes($event->invitation, $company_user, 'invoice', ['all_notifications', 'invoice_sent']);
@ -68,9 +68,9 @@ class InvoiceFailedEmailNotification implements ShouldQueue
$first_notification_sent = false; $first_notification_sent = false;
} }
$notification->method = $methods; // $notification->method = $methods;
$user->notify($notification); // $user->notify($notification);
} }
} }
} }

View File

@ -47,7 +47,7 @@ class InvitationViewedListener implements ShouldQueue
$entity_name = lcfirst(class_basename($event->entity)); $entity_name = lcfirst(class_basename($event->entity));
$invitation = $event->invitation; $invitation = $event->invitation;
$notification = new EntityViewedNotification($invitation, $entity_name); // $notification = new EntityViewedNotification($invitation, $entity_name);
$nmo = new NinjaMailerObject; $nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer( (new EntityViewedObject($invitation, $entity_name))->build() ); $nmo->mailable = new NinjaMailer( (new EntityViewedObject($invitation, $entity_name))->build() );
@ -68,16 +68,16 @@ class InvitationViewedListener implements ShouldQueue
} }
$notification->method = $methods; // $notification->method = $methods;
$company_user->user->notify($notification); // $company_user->user->notify($notification);
} }
if (isset($invitation->company->slack_webhook_url)) { // if (isset($invitation->company->slack_webhook_url)) {
$notification->method = ['slack']; // $notification->method = ['slack'];
// Notification::route('slack', $invitation->company->slack_webhook_url) // Notification::route('slack', $invitation->company->slack_webhook_url)
// ->notify($notification); // ->notify($notification);
} // }
} }
} }

View File

@ -69,19 +69,19 @@ class PaymentNotification implements ShouldQueue
NinjaMailerJob::dispatch($nmo); NinjaMailerJob::dispatch($nmo);
} }
$notification = new NewPaymentNotification($payment, $payment->company); // $notification = new NewPaymentNotification($payment, $payment->company);
$notification->method = $methods; // $notification->method = $methods;
if ($user) { // if ($user) {
$user->notify($notification); // $user->notify($notification);
} // }
} }
/*Company Notifications*/ /*Company Notifications*/
if (isset($payment->company->slack_webhook_url)) { // if (isset($payment->company->slack_webhook_url)) {
Notification::route('slack', $payment->company->slack_webhook_url) // Notification::route('slack', $payment->company->slack_webhook_url)
->notify(new NewPaymentNotification($payment, $payment->company, true)); // ->notify(new NewPaymentNotification($payment, $payment->company, true));
} // }
/*Google Analytics Track Revenue*/ /*Google Analytics Track Revenue*/
if (isset($payment->company->google_analytics_key)) { if (isset($payment->company->google_analytics_key)) {

View File

@ -53,7 +53,7 @@ class QuoteEmailedNotification implements ShouldQueue
foreach ($event->invitation->company->company_users as $company_user) { foreach ($event->invitation->company->company_users as $company_user) {
$user = $company_user->user; $user = $company_user->user;
$notification = new EntitySentNotification($event->invitation, 'quote'); // $notification = new EntitySentNotification($event->invitation, 'quote');
$methods = $this->findUserNotificationTypes($event->invitation, $company_user, 'quote', ['all_notifications', 'quote_sent']); $methods = $this->findUserNotificationTypes($event->invitation, $company_user, 'quote', ['all_notifications', 'quote_sent']);
@ -68,9 +68,9 @@ class QuoteEmailedNotification implements ShouldQueue
$first_notification_sent = false; $first_notification_sent = false;
} }
$notification->method = $methods; // $notification->method = $methods;
$user->notify($notification); // $user->notify($notification);
} }
} }
} }

View File

@ -131,6 +131,7 @@ class EntityFailedSendObject
'client' => $this->contact->present()->name(), 'client' => $this->contact->present()->name(),
'invoice' => $this->entity->number, 'invoice' => $this->entity->number,
'error' => $this->message, 'error' => $this->message,
'contact' => $this->contact->present()->name(),
] ]
), ),
'url' => $this->invitation->getAdminLink(), 'url' => $this->invitation->getAdminLink(),

View File

@ -24,9 +24,6 @@ class DownloadInvoices extends Mailable
/** /**
* Build the message. * Build the message.
*
* @return $this
* @throws \Laracasts\Presenter\Exceptions\PresenterException
*/ */
public function build() public function build()
{ {

View File

@ -20,6 +20,14 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
/**
* Class BaseModel
*
* @method scope() static
*
* @package App\Models
*/
class BaseModel extends Model class BaseModel extends Model
{ {
use MakesHash; use MakesHash;

View File

@ -27,6 +27,13 @@ use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Laracasts\Presenter\PresentableTrait; use Laracasts\Presenter\PresentableTrait;
/**
* Class ClientContact
*
* @method scope() static
*
* @package App\Models
*/
class ClientContact extends Authenticatable implements HasLocalePreference class ClientContact extends Authenticatable implements HasLocalePreference
{ {
use Notifiable; use Notifiable;
@ -88,6 +95,27 @@ class ClientContact extends Authenticatable implements HasLocalePreference
'client_id', '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() public function getEntityType()
{ {
return self::class; return self::class;

View File

@ -131,4 +131,9 @@ class Document extends BaseModel
{ {
return Storage::disk($this->disk)->url($this->url); return Storage::disk($this->disk)->url($this->url);
} }
public function diskPath(): string
{
return Storage::disk($this->disk)->path($this->url);
}
} }

View File

@ -22,9 +22,8 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
//@deprecated //@deprecated
class EntitySentNotification extends Notification implements ShouldQueue class EntitySentNotification extends Notification
{ {
//use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** /**
* Create a new notification instance. * Create a new notification instance.

View File

@ -21,9 +21,8 @@ use Illuminate\Notifications\Notification;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class EntityViewedNotification extends Notification implements ShouldQueue class EntityViewedNotification extends Notification
{ {
//use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** /**
* Create a new notification instance. * Create a new notification instance.

View File

@ -21,9 +21,8 @@ use Illuminate\Notifications\Notification;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class NewPaymentNotification extends Notification implements ShouldQueue class NewPaymentNotification extends Notification
{ {
// use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** /**
* Create a new notification instance. * Create a new notification instance.

View File

@ -28,6 +28,7 @@ class BaseRepository
{ {
use MakesHash; use MakesHash;
use SavesDocuments; use SavesDocuments;
public $import_mode = false;
/** /**
* @param $entity * @param $entity

View File

@ -28,15 +28,13 @@ use Illuminate\Support\Carbon;
/** /**
* PaymentRepository. * PaymentRepository.
*/ */
class PaymentRepository extends BaseRepository class PaymentRepository extends BaseRepository {
{
use MakesHash; use MakesHash;
use SavesDocuments; use SavesDocuments;
protected $credit_repo; protected $credit_repo;
public function __construct(CreditRepository $credit_repo) public function __construct( CreditRepository $credit_repo ) {
{
$this->credit_repo = $credit_repo; $this->credit_repo = $credit_repo;
} }
@ -152,8 +150,8 @@ class PaymentRepository extends BaseRepository
} }
} }
if (!$is_existing_payment) { if ( ! $is_existing_payment && ! $this->import_mode ) {
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); event( new PaymentWasCreated( $payment, $payment->company, Ninja::eventVars() ) );
} }
nlog("payment amount = {$payment->amount}"); nlog("payment amount = {$payment->amount}");

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -421,6 +421,10 @@ class Design extends BaseDesign
public function tableTotals(): array public function tableTotals(): array
{ {
$_variables = array_key_exists('variables', $this->context)
? $this->context['variables']
: ['values' => ['$entity.public_notes' => nl2br($this->entity->public_notes), '$entity.terms' => $this->entity->terms, '$entity_footer' => $this->entity->footer], 'labels' => []];
if ($this->type == 'delivery_note') { if ($this->type == 'delivery_note') {
return []; return [];
} }
@ -429,10 +433,11 @@ class Design extends BaseDesign
$elements = [ $elements = [
['element' => 'div', 'properties' => ['style' => 'display: flex; flex-direction: column;'], 'elements' => [ ['element' => 'div', 'properties' => ['style' => 'display: flex; flex-direction: column;'], 'elements' => [
['element' => 'p', 'content' => '$entity.public_notes', 'properties' => ['data-ref' => 'total_table-public_notes', 'style' => 'text-align: left;']], ['element' => 'p', 'content' => strtr($_variables['values']['$entity.public_notes'], $_variables), 'properties' => ['data-ref' => 'total_table-public_notes', 'style' => 'text-align: left;']],
['element' => 'p', 'content' => '', 'properties' => ['style' => 'text-align: left; display: flex; flex-direction: column;'], 'elements' => [ ['element' => 'p', 'content' => '', 'properties' => ['style' => 'text-align: left; display: flex; flex-direction: column;'], 'elements' => [
['element' => 'span', 'content' => '$entity.terms_label: ', 'properties' => ['hidden' => $this->entityVariableCheck('$entity.terms'), 'data-ref' => 'total_table-terms-label', 'style' => 'font-weight: bold; text-align: left;']], ['element' => 'span', 'content' => '$entity.terms_label: ', 'properties' => ['hidden' => $this->entityVariableCheck('$entity.terms'), 'data-ref' => 'total_table-terms-label', 'style' => 'font-weight: bold; text-align: left;']],
['element' => 'span', 'content' => '$entity.terms', 'properties' => ['data-ref' => 'total_table-terms', 'style' => 'text-align: left;']], ['element' => 'span', 'content' => strtr($_variables['values']['$entity.terms'], $_variables), 'properties' => ['data-ref' => 'total_table-terms', 'style' => 'text-align: left;']],
['element' => 'span', 'content' => strtr($_variables['values']['$entity_footer'], $_variables), 'properties' => ['data-ref' => 'total_table-footer', 'style' => 'text-align: left;']],
]], ]],
['element' => 'img', 'properties' => ['hidden' => $this->client->getSetting('signature_on_pdf'), 'style' => 'max-width: 50%; height: auto;', 'src' => '$contact.signature']], ['element' => 'img', 'properties' => ['hidden' => $this->client->getSetting('signature_on_pdf'), 'style' => 'max-width: 50%; height: auto;', 'src' => '$contact.signature']],
['element' => 'div', 'properties' => ['style' => 'margin-top: 1.5rem; display: flex; align-items: flex-start;'], 'elements' => [ ['element' => 'div', 'properties' => ['style' => 'margin-top: 1.5rem; display: flex; align-items: flex-start;'], 'elements' => [

View File

@ -121,7 +121,7 @@ class HtmlEngine
if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') { if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') {
$data['$entity'] = ['value' => '', 'label' => ctrans('texts.invoice')]; $data['$entity'] = ['value' => '', 'label' => ctrans('texts.invoice')];
$data['$number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.invoice_number')]; $data['$number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.invoice_number')];
$data['$entity.terms'] = ['value' => $this->entity->terms ?: '&nbsp;', 'label' => ctrans('texts.invoice_terms')]; $data['$entity.terms'] = ['value' => $this->entity->terms ?: '', 'label' => ctrans('texts.invoice_terms')];
$data['$terms'] = &$data['$entity.terms']; $data['$terms'] = &$data['$entity.terms'];
$data['$view_link'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_invoice').'</a>', 'label' => ctrans('texts.view_invoice')]; $data['$view_link'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_invoice').'</a>', 'label' => ctrans('texts.view_invoice')];
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')]; $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')];
@ -129,8 +129,8 @@ class HtmlEngine
if ($this->entity_string == 'quote') { if ($this->entity_string == 'quote') {
$data['$entity'] = ['value' => '', 'label' => ctrans('texts.quote')]; $data['$entity'] = ['value' => '', 'label' => ctrans('texts.quote')];
$data['$number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.quote_number')]; $data['$number'] = ['value' => $this->entity->number ?: '', 'label' => ctrans('texts.quote_number')];
$data['$entity.terms'] = ['value' => $this->entity->terms ?: '&nbsp;', 'label' => ctrans('texts.quote_terms')]; $data['$entity.terms'] = ['value' => $this->entity->terms ?: '', 'label' => ctrans('texts.quote_terms')];
$data['$terms'] = &$data['$entity.terms']; $data['$terms'] = &$data['$entity.terms'];
$data['$view_link'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_quote').'</a>', 'label' => ctrans('texts.view_quote')]; $data['$view_link'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_quote').'</a>', 'label' => ctrans('texts.view_quote')];
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_quote')]; $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_quote')];
@ -138,8 +138,8 @@ class HtmlEngine
if ($this->entity_string == 'credit') { if ($this->entity_string == 'credit') {
$data['$entity'] = ['value' => '', 'label' => ctrans('texts.credit')]; $data['$entity'] = ['value' => '', 'label' => ctrans('texts.credit')];
$data['$number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.credit_number')]; $data['$number'] = ['value' => $this->entity->number ?: '', 'label' => ctrans('texts.credit_number')];
$data['$entity.terms'] = ['value' => $this->entity->terms ?: '&nbsp;', 'label' => ctrans('texts.credit_terms')]; $data['$entity.terms'] = ['value' => $this->entity->terms ?: '', 'label' => ctrans('texts.credit_terms')];
$data['$terms'] = &$data['$entity.terms']; $data['$terms'] = &$data['$entity.terms'];
$data['$view_link'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_credit').'</a>', 'label' => ctrans('texts.view_credit')]; $data['$view_link'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_credit').'</a>', 'label' => ctrans('texts.view_credit')];
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_credit')]; $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_credit')];

View File

@ -13,7 +13,7 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', ''), 'app_domain' => env('APP_DOMAIN', ''),
'app_version' => '5.1.6', 'app_version' => '5.1.7',
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false), 'api_secret' => env('API_SECRET', false),

3134
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,23 +13,23 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/compat-data": "7.9.0", "@babel/compat-data": "7.9.0",
"@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-class-properties": "^7.12.13",
"cypress": "^4.12.1", "cypress": "^4.12.1",
"laravel-mix-purgecss": "^5.0.0", "laravel-mix-purgecss": "^5.0.0",
"vue-template-compiler": "^2.6.11" "vue-template-compiler": "^2.6.12"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/ui": "^0.1.4", "@tailwindcss/ui": "^0.7",
"axios": "^0.19", "axios": "^0.21.1",
"card-js": "^1.0.13", "card-js": "^1.0.13",
"card-validator": "^6.2.0", "card-validator": "^6.2.0",
"cross-env": "^7.0", "cross-env": "^7.0.3",
"jsignature": "^2.1.3", "jsignature": "^2.1.3",
"laravel-mix": "^5.0.7", "laravel-mix": "^5.0.9",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"resolve-url-loader": "^3.1.2", "resolve-url-loader": "^3.1.2",
"sass": "^1.26.10", "sass": "^1.32.7",
"sass-loader": "^8.0.0", "sass-loader": "^8.0.0",
"tailwindcss": "^1.6.2" "tailwindcss": "^1.9.6"
} }
} }

View File

@ -15918,38 +15918,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
skia skia
Copyright 2012 Intel Inc.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
skia
Copyright 2012 The Android Open Source Project Copyright 2012 The Android Open Source Project
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
@ -16881,6 +16849,38 @@ skia
Copyright 2021 Google LLC. Copyright 2021 Google LLC.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
skia
Copyright 2021 Google, LLC
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are modification, are permitted provided that the following conditions are
met: met:

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -3,11 +3,11 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache'; const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache'; const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = { const RESOURCES = {
"main.dart.js": "a36ad0c3c8bb9aa6c244bdfd4c7fdb2b", "main.dart.js": "5e63c564cc944e8930bd80a70ea4e156",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed", "icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35", "icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"favicon.ico": "51636d3a390451561744c42188ccd628", "favicon.ico": "51636d3a390451561744c42188ccd628",
"assets/NOTICES": "3bf182b8c943ec950bd5bfa72a82c6e0", "assets/NOTICES": "1c2603c5ad0cd648934fe488850b4dea",
"assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac", "assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f", "assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/AssetManifest.json": "659dcf9d1baf3aed3ab1b9c42112bf8f", "assets/AssetManifest.json": "659dcf9d1baf3aed3ab1b9c42112bf8f",
@ -49,7 +49,7 @@ self.addEventListener("install", (event) => {
return event.waitUntil( return event.waitUntil(
caches.open(TEMP).then((cache) => { caches.open(TEMP).then((cache) => {
return cache.addAll( return cache.addAll(
CORE.map((value) => new Request(value + '?revision=' + RESOURCES[value], {'cache': 'reload'}))); CORE.map((value) => new Request(value, {'cache': 'reload'})));
}) })
); );
}); });

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

207274
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"/js/app.js": "/js/app.js?id=1ee684e58f9f6eb754d5", "/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
"/css/app.css": "/css/app.css?id=599b11149976e86c83a3", "/css/app.css": "/css/app.css?id=1ea5400f7ae7b45f050c",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4", "/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1", "/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
"/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=206d7de4ac97612980ff", "/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=206d7de4ac97612980ff",
@ -15,6 +15,6 @@
"/js/clients/quotes/approve.js": "/js/clients/quotes/approve.js?id=85bcae0a646882e56b12", "/js/clients/quotes/approve.js": "/js/clients/quotes/approve.js?id=85bcae0a646882e56b12",
"/js/clients/shared/multiple-downloads.js": "/js/clients/shared/multiple-downloads.js?id=5c35d28cf0a3286e7c45", "/js/clients/shared/multiple-downloads.js": "/js/clients/shared/multiple-downloads.js?id=5c35d28cf0a3286e7c45",
"/js/clients/shared/pdf.js": "/js/clients/shared/pdf.js?id=fa54bb4229aba6b0817c", "/js/clients/shared/pdf.js": "/js/clients/shared/pdf.js?id=fa54bb4229aba6b0817c",
"/js/setup/setup.js": "/js/setup/setup.js?id=8cb5e2bb0d404725c20a", "/js/setup/setup.js": "/js/setup/setup.js?id=44bc4a71fc1d3606fc8e",
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ad" "/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ad"
} }

View File

@ -4140,7 +4140,9 @@ $LANG = array(
'start_migration' => 'Start Migration', 'start_migration' => 'Start Migration',
'recurring_cancellation_request' => 'Request for recurring invoice cancellation from :contact', 'recurring_cancellation_request' => 'Request for recurring invoice cancellation from :contact',
'recurring_cancellation_request_body' => ':contact from Client :client requested to cancel Recurring Invoice :invoice', 'recurring_cancellation_request_body' => ':contact from Client :client requested to cancel Recurring Invoice :invoice',
'hello' => 'Hello',
'group_documents' => 'Group documents',
'quote_approval_confirmation_label' => 'Are you sure you want to approve this quote?',
); );
return $LANG; return $LANG;

View File

@ -1,4 +1,4 @@
@component('email.template.master', ['design' => 'light', 'settings' => $settings]) @component('email.template.master', ['design' => 'light', 'settings' => $company->settings])
@slot('header') @slot('header')
@include('email.components.header', ['logo' => 'https://www.invoiceninja.com/wp-content/uploads/2015/10/logo-white-horizontal-1.png']) @include('email.components.header', ['logo' => 'https://www.invoiceninja.com/wp-content/uploads/2015/10/logo-white-horizontal-1.png'])
@endslot @endslot
@ -74,18 +74,34 @@
@endif @endif
<p><b>Data Quality:</b></p> <p><b>Data Quality:</b></p>
<p> {!! $check_data !!} </p> <p> {!! $check_data !!} </p>
@if(!empty($errors) )
<p>The following import errors occurred:</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Data</th>
<th>Error</th>
</tr>
</thead>
<tbody>
@foreach($errors as $entityType=>$entityErrors)
@foreach($entityErrors as $error)
<tr>
<td>{{$entityType}}</td>
<td>{{json_encode($error[$entityType]??null)}}</td>
<td>{{json_encode($error['error'])}}</td>
</tr>
@endforeach
@endforeach
</tbody>
</table>
@endif
<a href="{{ url('/') }}" target="_blank" class="button">{{ ctrans('texts.account_login')}}</a> <a href="{{ url('/') }}" target="_blank" class="button">{{ ctrans('texts.account_login')}}</a>
<p>{{ ctrans('texts.email_signature')}}<br/> {{ ctrans('texts.email_from') }}</p> <p>{{ ctrans('texts.email_signature')}}<br/> {{ ctrans('texts.email_from') }}</p>
@if(!$whitelabel)
@slot('footer')
@component('email.components.footer', ['url' => 'https://invoiceninja.com', 'url_text' => '&copy; InvoiceNinja'])
For any info, please visit InvoiceNinja.
@endcomponent
@endslot
@endif
@endcomponent @endcomponent

View File

@ -259,4 +259,4 @@
<table id="delivery-note-table" cellspacing="0"></table> <table id="delivery-note-table" cellspacing="0"></table>
</div> </div>
<div id="footer">$entity_footer</div> <div id="footer"></div>

View File

@ -217,4 +217,4 @@
<table id="delivery-note-table" cellspacing="0"></table> <table id="delivery-note-table" cellspacing="0"></table>
</div> </div>
<div id="footer">$entity_footer</div> <div id="footer"></div>

View File

@ -215,4 +215,4 @@
<table id="delivery-note-table" cellspacing="0"></table> <table id="delivery-note-table" cellspacing="0"></table>
</div> </div>
<div id="footer">$entity_footer</div> <div id="footer"></div>

View File

@ -215,5 +215,5 @@
<table id="delivery-note-table" cellspacing="0"></table> <table id="delivery-note-table" cellspacing="0"></table>
</div> </div>
<div id="footer">$entity_footer</div> <div id="footer"></div>

View File

@ -249,4 +249,4 @@
<table id="delivery-note-table" cellspacing="0"></table> <table id="delivery-note-table" cellspacing="0"></table>
</div> </div>
<div id="footer">$entity_footer</div> <div id="footer"></div>

View File

@ -235,7 +235,7 @@
<div style="margin-top: 195px"></div> <div style="margin-top: 195px"></div>
<div class="footer-wrapper" id="footer"> <div class="footer-wrapper" id="footer">
<div class="footer-content"> <div class="footer-content">
<div>$entity_footer</div> <div></div>
<div id="company-details"></div> <div id="company-details"></div>
<div id="company-address"></div> <div id="company-address"></div>
</div> </div>

View File

@ -180,4 +180,4 @@
<table id="delivery-note-table" cellspacing="0"></table> <table id="delivery-note-table" cellspacing="0"></table>
</div> </div>
<div id="footer">$entity_footer</div> <div id="footer"></div>

View File

@ -285,7 +285,7 @@
</div> </div>
<div id="footer"> <div id="footer">
$entity_footer
<div style="background-color: #00968B"><!-- 1 --></div> <div style="background-color: #00968B"><!-- 1 --></div>
<div style="background-color: #1D756E"><!-- 2 --></div> <div style="background-color: #1D756E"><!-- 2 --></div>

View File

@ -20,7 +20,7 @@
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="website" class="input-label">{{ ctrans('texts.website') }}</label> <label for="website" class="input-label">{{ ctrans('texts.website') }}</label>
<input id="website" class="input w-full" name="last_name" /> <input id="website" class="input w-full" name="website" />
@error('website') @error('website')
<div class="validation validation-fail"> <div class="validation validation-fail">
{{ $message }} {{ $message }}

View File

@ -3,7 +3,7 @@
@section('body') @section('body')
<div class="grid lg:grid-cols-12 py-8"> <div class="grid lg:grid-cols-12 py-8">
<div class="col-span-4 col-start-5"> <div class="lg:col-span-4 lg:col-start-5 px-6">
<div class="flex justify-center"> <div class="flex justify-center">
<img class="h-32 w-auto" src="{{ $company->present()->logo() }}" alt="{{ ctrans('texts.logo') }}"> <img class="h-32 w-auto" src="{{ $company->present()->logo() }}" alt="{{ ctrans('texts.logo') }}">
</div> </div>
@ -36,7 +36,7 @@
</span> </span>
</span> </span>
<button class="button button-primary bg-blue-600">{{ ctrans('texts.save') }}</button> <button class="button button-primary bg-blue-600">{{ ctrans('texts.register') }}</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -15,16 +15,6 @@
<svg class="md:hidden" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="8 17 12 21 16 17"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"></path></svg> <svg class="md:hidden" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="8 17 12 21 16 17"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"></path></svg>
</button> </button>
</div> </div>
<div class="flex items-center">
<div class="mr-3">
<input wire:click="statusChange('resources')" type="checkbox" class="form-checkbox cursor-pointer" id="resources-checkbox" checked>
<label for="resources-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.resources') }}</label>
</div>
<div class="mr-3">
<input wire:click="statusChange('client')" type="checkbox" class="form-checkbox cursor-pointer" id="client-checkbox">
<label for="client-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.client') }}</label>
</div>
</div>
</div> </div>
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8"> <div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div class="align-middle inline-block min-w-full overflow-hidden rounded"> <div class="align-middle inline-block min-w-full overflow-hidden rounded">
@ -37,11 +27,6 @@
{{ ctrans('texts.name') }} {{ ctrans('texts.name') }}
</span> </span>
</th> </th>
<th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider">
<span role="button" wire:click="sortBy('documentable_type')" class="cursor-pointer">
{{ ctrans('texts.resource') }}
</span>
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider"> <th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider">
<span role="button" wire:click="sortBy('type')" class="cursor-pointer"> <span role="button" wire:click="sortBy('type')" class="cursor-pointer">
{{ ctrans('texts.type') }} {{ ctrans('texts.type') }}
@ -55,7 +40,7 @@
<th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider"> <th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider">
{{ ctrans('texts.download') }} {{ ctrans('texts.download') }}
</th> </th>
<th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider" /> <th class="px-6 py-3 border-b border-gray-200 bg-primary text-left text-xs leading-4 font-medium text-white uppercase tracking-wider"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -67,9 +52,6 @@
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500"> <td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ Illuminate\Support\Str::limit($document->name, 20) }} {{ Illuminate\Support\Str::limit($document->name, 20) }}
</td> </td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ ((new \ReflectionClass($document->documentable))->getShortName()) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500"> <td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ App\Models\Document::$types[$document->type]['mime'] }} {{ App\Models\Document::$types[$document->type]['mime'] }}
</td> </td>

View File

@ -12,5 +12,102 @@
@endsection @endsection
@section('body') @section('body')
Coming soon. <div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.hello') }}, {{ $contact->first_name }}
</h3>
<div class="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2">
<div class="bg-white overflow-hidden shadow rounded">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm leading-5 font-medium text-gray-500 truncate">
{{ ctrans('texts.paid_to_date') }}
</dt>
<dd class="mt-1 text-3xl leading-9 font-semibold text-gray-900">
{{ App\Utils\Number::formatMoney($client->paid_to_date, $client) }}
</dd>
</dl>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm leading-5 font-medium text-gray-500 truncate">
{{ ctrans('texts.open_balance') }}
</dt>
<dd class="mt-1 text-3xl leading-9 font-semibold text-gray-900">
{{ App\Utils\Number::formatMoney($client->balance, $client) }}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="grid md:grid-cols-12 gap-4 mt-6">
<div class="col-span-6">
<div class="bg-white rounded shadow px-4 py-5 border-b border-gray-200 sm:px-6">
<div class="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-no-wrap">
<div class="ml-4 mt-4 w-full">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4 capitalize">
{{ ctrans('texts.group_documents') }}
</h3>
<div class="flex flex-col h-auto overflow-y-auto">
@if($client->group_settings)
@forelse($client->group_settings->documents as $document)
<a href="{{ route('client.documents.show', $document->hashed_id) }}" target="_blank"
class="block inline-flex items-center text-sm button-link text-primary">
<span>{{ Illuminate\Support\Str::limit($document->name, 40) }}</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="ml-2 text-primary h-6 w-4">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</a>
@empty
<p class="text-sm">{{ ctrans('texts.no_records_found') }}.</p>
@endforelse
@endif
</div>
</div>
</div>
</div>
</div>
<div class="col-span-6">
<div class="bg-white rounded shadow px-4 py-5 border-b border-gray-200 sm:px-6">
<div class="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-no-wrap">
<div class="ml-4 mt-4 w-full">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4 capitalize">
{{ ctrans('texts.default_documents') }}
</h3>
<div class="flex flex-col h-auto overflow-y-auto">
@forelse($client->company->documents as $document)
<a href="{{ route('client.documents.show', $document->hashed_id) }}" target="_blank"
class="block inline-flex items-center text-sm button-link text-primary">
<span>{{ Illuminate\Support\Str::limit($document->name, 40) }}</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="ml-2 text-primary h-6 w-4">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</a>
@empty
<p class="text-sm">{{ ctrans('texts.no_records_found') }}.</p>
@endforelse
</div>
</div>
</div>
</div>
</div>
</div>
@endsection @endsection

View File

@ -13,5 +13,6 @@
<form action="{{ route('client.documents.download_multiple') }}" method="post" id="multiple-downloads"> <form action="{{ route('client.documents.download_multiple') }}" method="post" id="multiple-downloads">
@csrf @csrf
</form> </form>
@livewire('documents-table', ['client' => $client]) @livewire('documents-table', ['client' => $client])
@endsection @endsection

View File

@ -43,7 +43,7 @@
</div> </div>
</form> </form>
@else @else
<div class="bg-white shadow sm:rounded-lg mb-4" translate> <div class="bg-white shadow sm:rounded-lg mb-4">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between"> <div class="sm:flex sm:items-start sm:justify-between">
<div> <div>
@ -57,7 +57,37 @@
</div> </div>
@endif @endif
<div class="flex items-center justify-between"> @if($invoice->documents->count() > 0)
<div class="bg-white shadow sm:rounded-lg mb-4">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<p class="text-lg leading-6 font-medium text-gray-900">{{ ctrans('texts.attachments') }}:</p>
@foreach($invoice->documents as $document)
<div class="inline-flex items-center space-x-1">
<a href="{{ route('client.documents.show', $document->hashed_id) }}" target="_blank"
class="block text-sm button-link text-primary">{{ Illuminate\Support\Str::limit($document->name, 40) }}</a>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="text-primary h-6 w-4">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
@if(!$loop->last)
<span>&mdash;</span>
@endif
</div>
@endforeach
</div>
</div>
</div>
</div>
@endif
<div class="flex items-center justify-between mt-4">
<section class="flex items-center"> <section class="flex items-center">
<div class="items-center" style="display: none" id="pagination-button-container"> <div class="items-center" style="display: none" id="pagination-button-container">
<button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300" id="previous-page-button" title="Previous page"> <button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300" id="previous-page-button" title="Previous page">

View File

@ -1,6 +1,7 @@
<form action="{{ route('client.quotes.bulk') }}" method="post"> <form action="{{ route('client.quotes.bulk') }}" method="post" onsubmit="return confirm('{{ ctrans('texts.quote_approval_confirmation_label') }}')">
@csrf @csrf
<input type="hidden" name="action" value="approve"> <input type="hidden" name="action" value="approve">
<input type="hidden" name="process" value="true">
<input type="hidden" name="quotes[]" value="{{ $quote->hashed_id }}"> <input type="hidden" name="quotes[]" value="{{ $quote->hashed_id }}">
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white shadow sm:rounded-lg">
@ -11,7 +12,7 @@
{{ ctrans('texts.waiting_for_approval') }} {{ ctrans('texts.waiting_for_approval') }}
</h3> </h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500"> <div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p translate> <p>
{{ ctrans('texts.quote_still_not_approved') }} {{ ctrans('texts.quote_still_not_approved') }}
</p> </p>
</div> </div>

Some files were not shown because too many files have changed in this diff Show More