Merge pull request #9266 from turbo124/v5-develop

Improve Recurring Expense Filters
This commit is contained in:
David Bomba 2024-02-07 12:16:39 +11:00 committed by GitHub
commit 814ecb3387
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 661 additions and 647 deletions

View File

@ -18,7 +18,7 @@ jobs:
phpunit-versions: ['latest']
ci_node_total: [ 8 ]
ci_node_index: [ 0, 1, 2, 3, 4, 5, 6, 7]
laravel: [9.*]
laravel: [10.*]
dependency-version: [prefer-stable]
env:

View File

@ -32,12 +32,21 @@ class RecurringExpenseFilters extends QueryFilters
}
return $this->builder->where(function ($query) use ($filter) {
$query->where('public_notes', 'like', '%'.$filter.'%')
->orWhere('custom_value1', 'like', '%'.$filter.'%')
->orWhere('custom_value2', 'like', '%'.$filter.'%')
->orWhere('custom_value3', 'like', '%'.$filter.'%')
->orWhere('custom_value4', 'like', '%'.$filter.'%');
$query->where('number', 'like', '%' . $filter . '%')
->orWhere('amount', 'like', '%' . $filter . '%')
->orWhere('public_notes', 'like', '%' . $filter . '%')
->orWhere('custom_value1', 'like', '%' . $filter . '%')
->orWhere('custom_value2', 'like', '%' . $filter . '%')
->orWhere('custom_value3', 'like', '%' . $filter . '%')
->orWhere('custom_value4', 'like', '%' . $filter . '%')
->orWhereHas('category', function ($q) use ($filter) {
$q->where('name', 'like', '%' . $filter . '%');
})
->orWhereHas('vendor', function ($q) use ($filter) {
$q->where('name', 'like', '%' . $filter . '%');
});
});
}
public function number(string $number = ''): Builder
@ -49,6 +58,74 @@ class RecurringExpenseFilters extends QueryFilters
return $this->builder->where('number', $number);
}
/**
* Filter based on client status.
*
* Statuses we need to handle
* - all
* - logged
* - pending
* - invoiced
* - paid
* - unpaid
*
* @return Builder
*/
public function client_status(string $value = ''): Builder
{
if (strlen($value) == 0) {
return $this->builder;
}
$status_parameters = explode(',', $value);
if (in_array('all', $status_parameters)) {
return $this->builder;
}
$this->builder->where(function ($query) use ($status_parameters) {
if (in_array('logged', $status_parameters)) {
$query->orWhere(function ($query) {
$query->where('amount', '>', 0)
->whereNull('invoice_id')
->whereNull('payment_date')
->where('should_be_invoiced', false);
});
}
if (in_array('pending', $status_parameters)) {
$query->orWhere(function ($query) {
$query->where('should_be_invoiced', true)
->whereNull('invoice_id');
});
}
if (in_array('invoiced', $status_parameters)) {
$query->orWhere(function ($query) {
$query->whereNotNull('invoice_id');
});
}
if (in_array('paid', $status_parameters)) {
$query->orWhere(function ($query) {
$query->whereNotNull('payment_date');
});
}
if (in_array('unpaid', $status_parameters)) {
$query->orWhere(function ($query) {
$query->whereNull('payment_date');
});
}
});
// nlog($this->builder->toSql());
return $this->builder;
}
/**
* Sorts the list based on $sort.
*
@ -63,9 +140,33 @@ class RecurringExpenseFilters extends QueryFilters
return $this->builder;
}
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
if ($sort_col[0] == 'client_id' && in_array($sort_col[1], ['asc', 'desc'])) {
return $this->builder
->orderByRaw('ISNULL(client_id), client_id '. $sort_col[1])
->orderBy(\App\Models\Client::select('name')
->whereColumn('clients.id', 'expenses.client_id'), $sort_col[1]);
}
return $this->builder->orderBy($sort_col[0], $dir);
if ($sort_col[0] == 'vendor_id' && in_array($sort_col[1], ['asc', 'desc'])) {
return $this->builder
->orderByRaw('ISNULL(vendor_id), vendor_id '. $sort_col[1])
->orderBy(\App\Models\Vendor::select('name')
->whereColumn('vendors.id', 'expenses.vendor_id'), $sort_col[1]);
}
if ($sort_col[0] == 'category_id' && in_array($sort_col[1], ['asc', 'desc'])) {
return $this->builder
->orderByRaw('ISNULL(category_id), category_id '. $sort_col[1])
->orderBy(\App\Models\ExpenseCategory::select('name')
->whereColumn('expense_categories.id', 'expenses.category_id'), $sort_col[1]);
}
if (is_array($sort_col) && in_array($sort_col[1], ['asc', 'desc']) && in_array($sort_col[0], ['public_notes', 'date', 'id_number', 'custom_value1', 'custom_value2', 'custom_value3', 'custom_value4'])) {
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
}
return $this->builder;
}
/**

View File

@ -244,19 +244,23 @@ class ImportController extends Controller
*/
public function detectDelimiter($csvfile): string
{
$delimiters = [',', '.', ';'];
$bestDelimiter = ' ';
$delimiters = [',', '.', ';', '|'];
$bestDelimiter = ',';
$count = 0;
// 10-01-2024 - A better way to resolve the csv file delimiter.
$csvfile = substr($csvfile, 0, strpos($csvfile, "\n"));
foreach ($delimiters as $delimiter) {
if (substr_count(strstr($csvfile, "\n", true), $delimiter) >= $count) {
$count = substr_count(strstr($csvfile, "\n", true), $delimiter);
$count = substr_count($csvfile, $delimiter);
$bestDelimiter = $delimiter;
}
}
return $bestDelimiter;
}
}

View File

@ -63,7 +63,6 @@ class TaskTransformer extends BaseTransformer
$time_log = collect($task_items_data)
->map(function ($item) {
return $this->parseLog($item);
})->toJson();
@ -80,10 +79,10 @@ class TaskTransformer extends BaseTransformer
$notes = $item['task.notes'] ?? '';
if(isset($item['task.is_billable']) && is_string($item['task.is_billable']) && in_array($item['task.is_billable'], ['yes', 'true', '1'])) {
if(isset($item['task.billable']) && is_string($item['task.billable']) && in_array($item['task.billable'], ['yes', 'true', '1', 'TRUE', 'YES'])) {
$is_billable = true;
} elseif(isset($item['task.is_billable']) && is_bool($item['task.is_billable'])) {
$is_billable = $item['task.is_billable'];
} elseif(isset($item['task.billable']) && is_bool($item['task.billable'])) {
$is_billable = $item['task.billable'];
} else {
$is_billable = false;
}

View File

@ -34,6 +34,7 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Hyvor\JsonExporter\File;
class CompanyExport implements ShouldQueue
{
@ -46,8 +47,8 @@ class CompanyExport implements ShouldQueue
private $export_format = 'json';
private $export_data = [];
private $writer;
private $file_name;
/**
* Create a new job instance.
*
@ -67,12 +68,16 @@ class CompanyExport implements ShouldQueue
public function handle()
{
MultiDB::setDb($this->company->db);
nlog("starting export");
$this->file_name = date('Y-m-d') . '_' . str_replace([" ", "/"], ["_",""], $this->company->present()->name() . '_' . $this->company->company_key . '.json');
$this->writer = new File($this->file_name);
set_time_limit(0);
$this->export_data['app_version'] = config('ninja.app_version');
$this->export_data['storage_url'] = Storage::url('');
$this->writer->value('app_version', config('ninja.app_version'));
$this->writer->value('storage_url', Storage::url(''));
$this->export_data['activities'] = $this->company->all_activities->map(function ($activity) {
$activity = $this->transformArrayOfKeys($activity, [
@ -98,27 +103,21 @@ class CompanyExport implements ShouldQueue
return $activity;
})->makeHidden(['id'])->all();
// $this->export_data['backups'] = $this->company->all_activities()->with('backup')->cursor()->map(function ($activity){
// $backup = $activity->backup;
$x = $this->writer->collection('activities');
$x->addItems($this->export_data['activities']);
$this->export_data = null;
// if(!$backup)
// return;
// $backup->activity_id = $this->encodePrimaryKey($backup->activity_id);
// return $backup;
// })->all();
$this->export_data['users'] = $this->company->users()->withTrashed()->cursor()->map(function ($user) {
$user->account_id = $this->encodePrimaryKey($user->account_id);
// $user->id = $this->encodePrimaryKey($user->id);
return $user->makeVisible(['id']);
})->all();
$x = $this->writer->collection('users');
$x->addItems($this->export_data['users']);
$this->export_data = null;
$this->export_data['client_contacts'] = $this->company->client_contacts->map(function ($client_contact) {
$client_contact = $this->transformArrayOfKeys($client_contact, ['company_id', 'user_id', 'client_id']);
@ -138,6 +137,10 @@ class CompanyExport implements ShouldQueue
})->all();
$x = $this->writer->collection('client_contacts');
$x->addItems($this->export_data['client_contacts']);
$this->export_data = null;
$this->export_data['client_gateway_tokens'] = $this->company->client_gateway_tokens->map(function ($client_gateway_token) {
$client_gateway_token = $this->transformArrayOfKeys($client_gateway_token, ['company_id', 'client_id', 'company_gateway_id']);
@ -145,6 +148,10 @@ class CompanyExport implements ShouldQueue
})->all();
$x = $this->writer->collection('client_gateway_tokens');
$x->addItems($this->export_data['client_gateway_tokens']);
$this->export_data = null;
$this->export_data['clients'] = $this->company->clients()->orderBy('number', 'DESC')->cursor()->map(function ($client) {
$client = $this->transformArrayOfKeys($client, ['company_id', 'user_id', 'assigned_user_id', 'group_settings_id']);
$client->tax_data = '';
@ -152,9 +159,20 @@ class CompanyExport implements ShouldQueue
})->all();
$x = $this->writer->collection('clients');
$x->addItems($this->export_data['clients']);
$this->export_data = null;
$this->export_data['company'] = $this->company->toArray();
$this->export_data['company']['company_key'] = $this->createHash();
$x = $this->writer->collection('company');
$x->addItems($this->export_data['company']);
$this->export_data = null;
$this->export_data['company_gateways'] = $this->company->company_gateways()->withTrashed()->cursor()->map(function ($company_gateway) {
$company_gateway = $this->transformArrayOfKeys($company_gateway, ['company_id', 'user_id']);
$company_gateway->config = decrypt($company_gateway->config);
@ -162,25 +180,49 @@ class CompanyExport implements ShouldQueue
return $company_gateway->makeVisible(['id']);
})->all();
$x = $this->writer->collection('company_gateways');
$x->addItems($this->export_data['company_gateways']);
$this->export_data = null;
$this->export_data['company_tokens'] = $this->company->tokens->map(function ($token) {
$token = $this->transformArrayOfKeys($token, ['company_id', 'account_id', 'user_id']);
return $token;
})->all();
$x = $this->writer->collection('company_tokens');
$x->addItems($this->export_data['company_tokens']);
$this->export_data = null;
$this->export_data['company_ledger'] = $this->company->ledger->map(function ($ledger) {
$ledger = $this->transformArrayOfKeys($ledger, ['activity_id', 'client_id', 'company_id', 'account_id', 'user_id','company_ledgerable_id']);
return $ledger;
})->all();
$x = $this->writer->collection('company_ledger');
$x->addItems($this->export_data['company_ledger']);
$this->export_data = null;
$this->export_data['company_users'] = $this->company->company_users()->without(['user','account'])->cursor()->map(function ($company_user) {
$company_user = $this->transformArrayOfKeys($company_user, ['company_id', 'account_id', 'user_id']);
// return $company_user->makeHidden(['react_settings']);
return $company_user;
})->all();
$x = $this->writer->collection('company_users');
$x->addItems($this->export_data['company_users']);
$this->export_data = null;
$this->export_data['credits'] = $this->company->credits()->orderBy('number', 'DESC')->cursor()->map(function ($credit) {
$credit = $this->transformBasicEntities($credit);
$credit = $this->transformArrayOfKeys($credit, ['recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id','invoice_id']);
@ -188,6 +230,10 @@ class CompanyExport implements ShouldQueue
return $credit->makeVisible(['id']);
})->all();
$x = $this->writer->collection('credits');
$x->addItems($this->export_data['credits']);
$this->export_data = null;
$this->export_data['credit_invitations'] = CreditInvitation::query()->where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($credit) {
$credit = $this->transformArrayOfKeys($credit, ['company_id', 'user_id', 'client_contact_id', 'credit_id']);
@ -195,8 +241,20 @@ class CompanyExport implements ShouldQueue
return $credit->makeVisible(['id']);
})->all();
$x = $this->writer->collection('credit_invitations');
$x->addItems($this->export_data['credit_invitations']);
$this->export_data = null;
$this->export_data['designs'] = $this->company->user_designs->makeHidden(['id'])->all();
$x = $this->writer->collection('designs');
$x->addItems($this->export_data['designs']);
$this->export_data = null;
$this->export_data['documents'] = $this->company->all_documents->map(function ($document) {
$document = $this->transformArrayOfKeys($document, ['user_id', 'assigned_user_id', 'company_id', 'project_id', 'vendor_id','documentable_id']);
$document->hashed_id = $this->encodePrimaryKey($document->id);
@ -204,12 +262,20 @@ class CompanyExport implements ShouldQueue
return $document->makeVisible(['id']);
})->all();
$x = $this->writer->collection('documents');
$x->addItems($this->export_data['documents']);
$this->export_data = null;
$this->export_data['expense_categories'] = $this->company->expense_categories()->cursor()->map(function ($expense_category) {
$expense_category = $this->transformArrayOfKeys($expense_category, ['user_id', 'company_id']);
return $expense_category->makeVisible(['id']);
})->all();
$x = $this->writer->collection('expense_categories');
$x->addItems($this->export_data['expense_categories']);
$this->export_data = null;
$this->export_data['expenses'] = $this->company->expenses()->orderBy('number', 'DESC')->cursor()->map(function ($expense) {
$expense = $this->transformBasicEntities($expense);
@ -218,6 +284,12 @@ class CompanyExport implements ShouldQueue
return $expense->makeVisible(['id']);
})->all();
$x = $this->writer->collection('expenses');
$x->addItems($this->export_data['expenses']);
$this->export_data = null;
$this->export_data['group_settings'] = $this->company->group_settings->map(function ($gs) {
$gs = $this->transformArrayOfKeys($gs, ['user_id', 'company_id']);
@ -225,6 +297,11 @@ class CompanyExport implements ShouldQueue
})->all();
$x = $this->writer->collection('group_settings');
$x->addItems($this->export_data['group_settings']);
$this->export_data = null;
$this->export_data['invoices'] = $this->company->invoices()->orderBy('number', 'DESC')->cursor()->map(function ($invoice) {
$invoice = $this->transformBasicEntities($invoice);
$invoice = $this->transformArrayOfKeys($invoice, ['recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id']);
@ -238,12 +315,22 @@ class CompanyExport implements ShouldQueue
})->all();
$x = $this->writer->collection('invoices');
$x->addItems($this->export_data['invoices']);
$this->export_data = null;
$this->export_data['invoice_invitations'] = InvoiceInvitation::query()->where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($invoice) {
$invoice = $this->transformArrayOfKeys($invoice, ['company_id', 'user_id', 'client_contact_id', 'invoice_id']);
return $invoice->makeVisible(['id']);
})->all();
$x = $this->writer->collection('invoice_invitations');
$x->addItems($this->export_data['invoice_invitations']);
$this->export_data = null;
$this->export_data['payment_terms'] = $this->company->user_payment_terms->map(function ($term) {
$term = $this->transformArrayOfKeys($term, ['user_id', 'company_id']);
@ -251,6 +338,11 @@ class CompanyExport implements ShouldQueue
})->makeHidden(['id'])->all();
$x = $this->writer->collection('payment_terms');
$x->addItems($this->export_data['payment_terms']);
$this->export_data = null;
$this->export_data['payments'] = $this->company->payments()->orderBy('number', 'DESC')->cursor()->map(function ($payment) {
$payment = $this->transformBasicEntities($payment);
$payment = $this->transformArrayOfKeys($payment, ['client_id','project_id', 'vendor_id', 'client_contact_id', 'invitation_id', 'company_gateway_id', 'transaction_id']);
@ -260,6 +352,13 @@ class CompanyExport implements ShouldQueue
return $payment->makeVisible(['id']);
})->all();
$x = $this->writer->collection('payments');
$x->addItems($this->export_data['payments']);
$this->export_data = null;
$this->export_data['products'] = $this->company->products->map(function ($product) {
$product = $this->transformBasicEntities($product);
$product = $this->transformArrayOfKeys($product, ['vendor_id','project_id']);
@ -267,6 +366,12 @@ class CompanyExport implements ShouldQueue
return $product->makeVisible(['id']);
})->all();
$x = $this->writer->collection('products');
$x->addItems($this->export_data['products']);
$this->export_data = null;
$this->export_data['projects'] = $this->company->projects()->orderBy('number', 'DESC')->cursor()->map(function ($project) {
$project = $this->transformBasicEntities($project);
$project = $this->transformArrayOfKeys($project, ['client_id']);
@ -274,6 +379,12 @@ class CompanyExport implements ShouldQueue
return $project->makeVisible(['id']);
})->all();
$x = $this->writer->collection('projects');
$x->addItems($this->export_data['projects']);
$this->export_data = null;
$this->export_data['quotes'] = $this->company->quotes()->orderBy('number', 'DESC')->cursor()->map(function ($quote) {
$quote = $this->transformBasicEntities($quote);
$quote = $this->transformArrayOfKeys($quote, ['invoice_id','recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id']);
@ -282,12 +393,23 @@ class CompanyExport implements ShouldQueue
})->all();
$x = $this->writer->collection('quotes');
$x->addItems($this->export_data['quotes']);
$this->export_data = null;
$this->export_data['quote_invitations'] = QuoteInvitation::query()->where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($quote) {
$quote = $this->transformArrayOfKeys($quote, ['company_id', 'user_id', 'client_contact_id', 'quote_id']);
return $quote->makeVisible(['id']);
})->all();
$x = $this->writer->collection('quote_invitations');
$x->addItems($this->export_data['quote_invitations']);
$this->export_data = null;
$this->export_data['recurring_expenses'] = $this->company->recurring_expenses()->orderBy('number', 'DESC')->cursor()->map(function ($expense) {
$expense = $this->transformBasicEntities($expense);
$expense = $this->transformArrayOfKeys($expense, ['vendor_id', 'invoice_id', 'client_id', 'category_id', 'project_id']);
@ -295,6 +417,13 @@ class CompanyExport implements ShouldQueue
return $expense->makeVisible(['id']);
})->all();
$x = $this->writer->collection('recurring_expenses');
$x->addItems($this->export_data['recurring_expenses']);
$this->export_data = null;
$this->export_data['recurring_invoices'] = $this->company->recurring_invoices()->orderBy('number', 'DESC')->cursor()->map(function ($ri) {
$ri = $this->transformBasicEntities($ri);
$ri = $this->transformArrayOfKeys($ri, ['client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id']);
@ -303,12 +432,24 @@ class CompanyExport implements ShouldQueue
})->all();
$x = $this->writer->collection('recurring_invoices');
$x->addItems($this->export_data['recurring_invoices']);
$this->export_data = null;
$this->export_data['recurring_invoice_invitations'] = RecurringInvoiceInvitation::query()->where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($ri) {
$ri = $this->transformArrayOfKeys($ri, ['company_id', 'user_id', 'client_contact_id', 'recurring_invoice_id']);
return $ri;
})->all();
$x = $this->writer->collection('recurring_invoice_invitations');
$x->addItems($this->export_data['recurring_invoice_invitations']);
$this->export_data = null;
$this->export_data['subscriptions'] = $this->company->subscriptions->map(function ($subscription) {
$subscription = $this->transformBasicEntities($subscription);
$subscription->group_id = $this->encodePrimaryKey($subscription->group_id);
@ -323,6 +464,10 @@ class CompanyExport implements ShouldQueue
})->all();
$x = $this->writer->collection('subscriptions');
$x->addItems($this->export_data['subscriptions']);
$this->export_data = null;
$this->export_data['system_logs'] = $this->company->system_logs->map(function ($log) {
$log->client_id = $this->encodePrimaryKey($log->client_id);
$log->company_id = $this->encodePrimaryKey($log->company_id);
@ -330,14 +475,26 @@ class CompanyExport implements ShouldQueue
return $log;
})->makeHidden(['id'])->all();
$x = $this->writer->collection('system_logs');
$x->addItems($this->export_data['system_logs']);
$this->export_data = null;
$this->export_data['tasks'] = $this->company->tasks()->orderBy('number', 'DESC')->cursor()->map(function ($task) {
$task = $this->transformBasicEntities($task);
$task = $this->transformArrayOfKeys($task, ['client_id', 'invoice_id', 'project_id', 'status_id']);
return $task->makeHidden(['hash','meta'])->makeVisible(['id']);
// return $task->makeHidden(['hash','meta'])->makeVisible(['id']); //@release v5.7.63
})->all();
$x = $this->writer->collection('tasks');
$x->addItems($this->export_data['tasks']);
$this->export_data = null;
$this->export_data['task_statuses'] = $this->company->task_statuses->map(function ($status) {
$status->id = $this->encodePrimaryKey($status->id);
$status->user_id = $this->encodePrimaryKey($status->user_id);
@ -346,6 +503,13 @@ class CompanyExport implements ShouldQueue
return $status;
})->all();
$x = $this->writer->collection('task_statuses');
$x->addItems($this->export_data['task_statuses']);
$this->export_data = null;
$this->export_data['tax_rates'] = $this->company->tax_rates->map(function ($rate) {
$rate->company_id = $this->encodePrimaryKey($rate->company_id);
$rate->user_id = $this->encodePrimaryKey($rate->user_id);
@ -353,11 +517,24 @@ class CompanyExport implements ShouldQueue
return $rate;
})->makeHidden(['id'])->all();
$x = $this->writer->collection('tax_rates');
$x->addItems($this->export_data['tax_rates']);
$this->export_data = null;
$this->export_data['vendors'] = $this->company->vendors()->orderBy('number', 'DESC')->cursor()->map(function ($vendor) {
return $this->transformBasicEntities($vendor)->makeVisible(['id']);
})->all();
$x = $this->writer->collection('vendors');
$x->addItems($this->export_data['vendors']);
$this->export_data = null;
$this->export_data['vendor_contacts'] = VendorContact::where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($vendor) {
$vendor = $this->transformBasicEntities($vendor);
$vendor = $this->transformArrayOfKeys($vendor, ['vendor_id']);
@ -365,6 +542,13 @@ class CompanyExport implements ShouldQueue
return $vendor->makeVisible(['id','user_id']);
})->all();
$x = $this->writer->collection('vendor_contacts');
$x->addItems($this->export_data['vendor_contacts']);
$this->export_data = null;
$this->export_data['webhooks'] = $this->company->webhooks->map(function ($hook) {
$hook->user_id = $this->encodePrimaryKey($hook->user_id);
$hook->company_id = $this->encodePrimaryKey($hook->company_id);
@ -372,6 +556,12 @@ class CompanyExport implements ShouldQueue
return $hook;
})->makeHidden(['id'])->all();
$x = $this->writer->collection('webhooks');
$x->addItems($this->export_data['webhooks']);
$this->export_data = null;
$this->export_data['purchase_orders'] = $this->company->purchase_orders()->orderBy('number', 'DESC')->cursor()->map(function ($purchase_order) {
$purchase_order = $this->transformBasicEntities($purchase_order);
$purchase_order = $this->transformArrayOfKeys($purchase_order, ['expense_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id','project_id']);
@ -384,6 +574,13 @@ class CompanyExport implements ShouldQueue
'company_id',]);
})->all();
$x = $this->writer->collection('purchase_orders');
$x->addItems($this->export_data['purchase_orders']);
$this->export_data = null;
$this->export_data['purchase_order_invitations'] = PurchaseOrderInvitation::query()->where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($purchase_order) {
$purchase_order = $this->transformArrayOfKeys($purchase_order, ['company_id', 'user_id', 'vendor_contact_id', 'purchase_order_id']);
@ -391,26 +588,46 @@ class CompanyExport implements ShouldQueue
})->all();
$x = $this->writer->collection('purchase_order_invitations');
$x->addItems($this->export_data['purchase_order_invitations']);
$this->export_data = null;
$this->export_data['bank_integrations'] = $this->company->bank_integrations()->withTrashed()->orderBy('id', 'ASC')->cursor()->map(function ($bank_integration) {
$bank_integration = $this->transformArrayOfKeys($bank_integration, ['account_id','company_id', 'user_id']);
return $bank_integration->makeVisible(['id','user_id','company_id','account_id','hashed_id']);
})->all();
$x = $this->writer->collection('bank_integrations');
$x->addItems($this->export_data['bank_integrations']);
$this->export_data = null;
$this->export_data['bank_transactions'] = $this->company->bank_transactions()->withTrashed()->orderBy('id', 'ASC')->cursor()->map(function ($bank_transaction) {
$bank_transaction = $this->transformArrayOfKeys($bank_transaction, ['company_id', 'user_id','bank_integration_id','expense_id','ninja_category_id','vendor_id']);
return $bank_transaction->makeVisible(['id','user_id','company_id']);
})->all();
$x = $this->writer->collection('bank_transactions');
$x->addItems($this->export_data['bank_transactions']);
$this->export_data = null;
$this->export_data['schedulers'] = $this->company->schedulers()->withTrashed()->orderBy('id', 'ASC')->cursor()->map(function ($scheduler) {
$scheduler = $this->transformArrayOfKeys($scheduler, ['company_id', 'user_id']);
return $scheduler->makeVisible(['id','user_id','company_id']);
})->all();
$x = $this->writer->collection('schedulers');
$x->addItems($this->export_data['schedulers']);
$this->export_data = null;
//write to tmp and email to owner();
$this->writer->end();
$this->zipAndSend();
return true;
@ -446,38 +663,42 @@ class CompanyExport implements ShouldQueue
private function zipAndSend()
{
$file_name = date('Y-m-d').'_'.str_replace([" ", "/"], ["_",""], $this->company->present()->name() . '_' . $this->company->company_key .'.zip');
// $file_name = date('Y-m-d').'_'.str_replace([" ", "/"], ["_",""], $this->company->present()->name() . '_' . $this->company->company_key .'.zip');
$path = 'backups';
$zip_path = \Illuminate\Support\Str::ascii(str_replace(".json", ".zip", $this->file_name));
// $path = 'backups';
// Storage::makeDirectory(storage_path('backups/'));
try {
mkdir(storage_path('backups/'));
} catch(\Exception $e) {
nlog("could not create directory");
}
// try {
// mkdir(storage_path('backups/'));
// } catch(\Exception $e) {
// nlog("could not create directory");
// }
$zip_path = storage_path('backups/'.\Illuminate\Support\Str::ascii($file_name));
// $zip_path = storage_path('backups/'.\Illuminate\Support\Str::ascii($file_name));
$zip = new \ZipArchive();
if ($zip->open($zip_path, \ZipArchive::CREATE) !== true) {
nlog("cannot open {$zip_path}");
}
$zip->addFromString("backup.json", json_encode($this->export_data));
$zip->addFile($this->file_name);
$zip->renameName($this->file_name, 'backup.json');
// $zip->addFromString("backup.json", json_encode($this->export_data));
$zip->close();
Storage::disk(config('filesystems.default'))->put('backups/'.$file_name, file_get_contents($zip_path));
Storage::disk(config('filesystems.default'))->put('backups/'.str_replace(".json", ".zip",$this->file_name), file_get_contents($zip_path));
if(file_exists($zip_path)) {
unlink($zip_path);
}
if(Ninja::isSelfHost()) {
$storage_path = 'backups/'.$file_name;
$storage_path = 'backups/'.str_replace(".json", ".zip",$this->file_name);
} else {
$storage_path = Storage::disk(config('filesystems.default'))->path('backups/'.$file_name);
$storage_path = Storage::disk(config('filesystems.default'))->path('backups/'.str_replace(".json", ".zip",$this->file_name));
}
$url = Cache::get($this->hash);

View File

@ -272,13 +272,10 @@ class CompanyImport implements ShouldQueue
nlog("Company ID = {$this->company->id}");
nlog("file_location ID = {$this->file_location}");
// $this->backup_file = Cache::get($this->hash);
if (empty($this->file_location)) {
throw new \Exception('No import data found, has the cache expired?');
}
// $this->backup_file = json_decode(file_get_contents($this->file_location));
$tmp_file = $this->unzipFile();
$this->file_path = $tmp_file;
@ -315,6 +312,7 @@ class CompanyImport implements ShouldQueue
}
unlink($tmp_file);
unlink($this->file_location);
}
//
@ -342,19 +340,31 @@ class CompanyImport implements ShouldQueue
$path = TempFile::filePath(Storage::disk(config('filesystems.default'))->get($this->file_location), basename($this->file_location));
$zip = new ZipArchive();
$archive = $zip->open($path);
$res = $zip->open($path);
$file_path = sys_get_temp_dir().'/'.sha1(microtime());
$zip->extractTo($file_path);
$zip->close();
$file_location = "{$file_path}/backup.json";
if ($res === true) {
echo "ok";
$extraction_res = $zip->extractTo($file_path);
nlog($extraction_res);
$closer = $zip->close();
nlog($closer);
} else {
echo "failed, code: " . $res;
}
$file_path = "{$file_path}/backup.json";
nlog($file_path);
if (! file_exists($file_path)) {
throw new NonExistingMigrationFile('Backup file does not exist, or is corrupted.');
}
return $file_location;
return $file_path;
}

View File

@ -260,12 +260,37 @@ class NinjaMailerJob implements ShouldQueue
$t->replace(Ninja::transformTranslations($this->nmo->settings));
/** Force free/trials onto specific mail driver */
if(Ninja::isHosted() && !$this->company->account->isPaid())
{
$this->mailer = 'mailgun';
$this->setHostedMailgunMailer();
// if(Ninja::isHosted() && !$this->company->account->isPaid())
// {
// $this->mailer = 'mailgun';
// $this->setHostedMailgunMailer();
// return $this;
// }
if(Ninja::isHosted() && $this->company->account->isPaid() && $this->nmo->settings->email_sending_method == 'default') {
//check if outlook.
try{
$email = $this->nmo->to_user->email;
$domain = explode("@", $email)[1] ?? "";
$dns = dns_get_record($domain, DNS_MX);
$server = $dns[0]["target"];
if(stripos($server, "outlook.com") !== false){
$this->mailer = 'postmark';
$this->client_postmark_secret = config('services.postmark-outlook.token');
$this->nmo
->mailable
->from('maildelivery@invoice.services', 'Invoice Ninja');
return $this;
}
}
catch(\Exception $e){
nlog($e->getMessage());
}
}
switch ($this->nmo->settings->email_sending_method) {
case 'default':

View File

@ -45,6 +45,7 @@ class QuoteCreatedNotification implements ShouldQueue
/* We loop through each user and determine whether they need to be notified */
/** @var \App\Models\CompanyUser $company_user */
foreach ($event->company->company_users as $company_user) {
/* The User */
$user = $company_user->user;

View File

@ -201,7 +201,7 @@ class CompanyUser extends Pivot
* @return bool
*/
public function portalType(): bool
{
{ nlog(isset($this->react_settings->react_notification_link) && $this->react_settings->react_notification_link);
return isset($this->react_settings->react_notification_link) && $this->react_settings->react_notification_link;
}

View File

@ -204,7 +204,7 @@ class Credit extends BaseModel
public function getDueDateAttribute($value)
{
return $this->dateMutator($value);
return $value ? $this->dateMutator($value) : null;
}
public function getPartialDueDateAttribute($value)

View File

@ -220,7 +220,7 @@ class Document extends BaseModel
$img = new \Imagick();
$img->readImageBlob($file);
$img->setImageCompression(true);
$img->setImageCompressionQuality(50);
$img->setImageCompressionQuality(40);
return $img->getImageBlob();

View File

@ -486,11 +486,35 @@ class Email implements ShouldQueue
{
/** Force free/trials onto specific mail driver */
if(Ninja::isHosted() && !$this->company->account->isPaid()) {
$this->mailer = 'mailgun';
$this->setHostedMailgunMailer();
// if(Ninja::isHosted() && !$this->company->account->isPaid()) {
// $this->mailer = 'mailgun';
// $this->setHostedMailgunMailer();
// return $this;
// }
if(Ninja::isHosted() && $this->company->account->isPaid() && $this->email_object->settings->email_sending_method == 'default') {
try {
$address_object = reset($this->email_object->to);
$email = $address_object->address ?? '';
$domain = explode("@", $email)[1] ?? "";
$dns = dns_get_record($domain, DNS_MX);
$server = $dns[0]["target"];
if(stripos($server, "outlook.com") !== false) {
$this->mailer = 'postmark';
$this->client_postmark_secret = config('services.postmark-outlook.token');
$this->mailable
->from('maildelivery@invoice.services', 'Invoice Ninja');
return $this;
}
} catch(\Exception $e) {
nlog($e->getMessage());
}
}
switch ($this->email_object->settings->email_sending_method) {
case 'default':

View File

@ -384,7 +384,7 @@ class HtmlEngine
$data['$total'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: ' ', 'label' => ctrans('texts.total')];
$data['$amount'] = &$data['$total'];
$data['$amount_due'] = ['value' => &$data['$total']['value'], 'label' => ctrans('texts.amount_due')];
$data['$amount_due'] = ['value' => &$data['$balance_due']['value'], 'label' => ctrans('texts.amount_due')];
$data['$quote.total'] = &$data['$total'];
$data['$invoice.total'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: ' ', 'label' => ctrans('texts.invoice_total')];
$data['$invoice_total_raw'] = ['value' => $this->entity_calc->getTotal(), 'label' => ctrans('texts.invoice_total')];

View File

@ -112,6 +112,7 @@
"fakerphp/faker": "^1.14",
"filp/whoops": "^2.7",
"friendsofphp/php-cs-fixer": "^3.14",
"hyvor/php-json-exporter": "^0.0.3",
"laracasts/cypress": "^3.0",
"larastan/larastan": "^2",
"mockery/mockery": "^1.4.4",
@ -119,7 +120,6 @@
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^10.0",
"spatie/laravel-ignition": "^2.0",
"spatie/laravel-open-telemetry": "^0.0.9",
"spaze/phpstan-stripe": "^3.0"
},
"autoload": {

708
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,10 @@ return [
'token' => env('POSTMARK_SECRET', ''),
],
'postmark-outlook' => [
'token' => env('POSTMARK_OUTLOOK_SECRET','')
],
'microsoft' => [
'client_id' => env('MICROSOFT_CLIENT_ID'),
'client_secret' => env('MICROSOFT_CLIENT_SECRET'),

View File

@ -0,0 +1,31 @@
<?php
use App\Models\Currency;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if($c = Currency::find(79)) {
$c->thousand_separator = ',';
$c->decimal_separator = '.';
$c->save();
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

View File

@ -101,7 +101,7 @@ class CurrenciesSeeder extends Seeder
['id' => 76, 'name' => 'Surinamese Dollar', 'code' => 'SRD', 'symbol' => 'SRD', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','],
['id' => 77, 'name' => 'Bahraini Dinar', 'code' => 'BHD', 'symbol' => 'BD ', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 78, 'name' => 'Venezuelan Bolivars', 'code' => 'VES', 'symbol' => 'Bs.', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','],
['id' => 79, 'name' => 'South Korean Won', 'code' => 'KRW', 'symbol' => 'W ', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','],
['id' => 79, 'name' => 'South Korean Won', 'code' => 'KRW', 'symbol' => 'W ', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 80, 'name' => 'Moroccan Dirham', 'code' => 'MAD', 'symbol' => 'MAD ', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 81, 'name' => 'Jamaican Dollar', 'code' => 'JMD', 'symbol' => '$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 82, 'name' => 'Angolan Kwanza', 'code' => 'AOA', 'symbol' => 'Kz', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','],

View File

@ -1,4 +1,4 @@
@component('email.template.admin', ['design' => 'light', 'settings' => $settings, 'logo' => $logo])
@component('email.template.admin', ['design' => 'light', 'settings' => $settings, 'logo' => $logo, 'url' => $url])
<div class="center">
@isset($greeting)
<p>{{ $greeting }}</p>

View File

@ -1,6 +1,7 @@
@php
$primary_color = isset($settings) ? $settings->primary_color : '#4caf50';
$email_alignment = isset($settings) && $settings?->email_alignment ? $settings->email_alignment : 'center';
$email_preferences = isset($url) && str_contains($url ?? '', '/#/') ? config('ninja.react_url').'/#/settings/user_details/notifications' : config('ninja.app_url');
@endphp
<!DOCTYPE html
@ -249,6 +250,13 @@
<p style="text-align: center; color: #ffffff; font-size: 10px;
font-family: Verdana, Geneva, Tahoma, sans-serif;"{{ date('Y') }} Invoice Ninja, All Rights Reserved
</p>
<a href="{{ $email_preferences }}">
<p style="text-align: center; color: #ffffff; font-size: 10px; font-family: Verdana, Geneva, Tahoma, sans-serif;">
{{ ctrans('texts.email_preferences') }}
</p>
</a>
</div>
</td>
</tr>

View File

@ -45,6 +45,6 @@
@endif
@endisset
@isset($email_preferences)
<p><a href="{!! $email_preferences !!}">{{ ctrans('texts.unsubscribe') }}</a></p>
@endisset
@if(isset($email_preferences) && $email_preferences)
<p><a href="{!! $email_preferences !!}">{{ ctrans('texts.email_preferences') }}</a></p>
@endif

View File

@ -389,11 +389,11 @@
</tr>
<tr class="header-payment-due-label">
<td>$payment_due_label:</td>
<td>$payment_due</td>
<td>$due_date</td>
</tr>
<tr>
<td class="header-amount-due-label">$amount_due_label:</td>
<td class="header-amount-due-value">$amount_due</td>
<td class="header-amount-due-value">$balance_due</td>
</tr>
</table>
</div>

View File

@ -64,7 +64,7 @@ class TaskImportTest extends TestCase
2 => 'client.name',
4 => 'task.number',
5 => 'task.description',
6 => 'task.is_billable',
6 => 'task.billable',
7 => 'task.start_date',
9 => 'task.end_date',
8 => 'task.start_time',
@ -132,7 +132,7 @@ class TaskImportTest extends TestCase
3 => 'project.name',
2 => 'client.name',
5 => 'task.description',
6 => 'task.is_billable',
6 => 'task.billable',
7 => 'task.start_date',
9 => 'task.end_date',
8 => 'task.start_time',