Merge branches 'feature-brevo' and 'feature-brevo' of https://github.com/paulwer/invoiceninja; branch 'v5-develop' of https://github.com/invoiceninja/invoiceninja into feature-brevo

This commit is contained in:
paulwer 2024-03-17 07:29:17 +01:00
commit e443fb28ec
216 changed files with 232921 additions and 218905 deletions

View File

@ -1 +1 @@
5.8.26 5.8.37

View File

@ -884,14 +884,14 @@ class CheckData extends Command
public function checkClientSettings() public function checkClientSettings()
{ {
if ($this->option('fix') == 'true') { if ($this->option('fix') == 'true') {
Client::query()->whereNull('country_id')->cursor()->each(function ($client) { Client::query()->whereNull('country_id')->orWhere('country_id', 0)->cursor()->each(function ($client) {
$client->country_id = $client->company->settings->country_id; $client->country_id = $client->company->settings->country_id;
$client->saveQuietly(); $client->saveQuietly();
$this->logMessage("Fixing country for # {$client->id}"); $this->logMessage("Fixing country for # {$client->id}");
}); });
Client::query()->whereNull("settings->currency_id")->cursor()->each(function ($client) { Client::query()->whereNull("settings->currency_id")->orWhereJsonContains('settings', ['currency_id' => ''])->cursor()->each(function ($client) {
$settings = $client->settings; $settings = $client->settings;
$settings->currency_id = (string)$client->company->settings->currency_id; $settings->currency_id = (string)$client->company->settings->currency_id;
$client->settings = $settings; $client->settings = $settings;
@ -933,7 +933,6 @@ class CheckData extends Command
}); });
Invoice::withTrashed() Invoice::withTrashed()
->where("partial", 0) ->where("partial", 0)
->whereNotNull("partial_due_date") ->whereNotNull("partial_due_date")
@ -947,7 +946,42 @@ class CheckData extends Command
}); });
Company::whereDoesntHave('company_users', function ($query){
$query->where('is_owner', 1);
})
->cursor()
->when(Ninja::isHosted())
->each(function ($c){
$this->logMessage("Orphan Account # {$c->account_id}");
});
CompanyUser::whereDoesntHave('tokens')
->cursor()
->when(Ninja::isHosted())
->each(function ($cu){
$this->logMessage("Missing tokens for Company User # {$cu->id}");
});
CompanyUser::whereDoesntHave('user')
->cursor()
->when(Ninja::isHosted())
->each(function ($cu) {
$this->logMessage("Missing user for Company User # {$cu->id}");
});
$cus = CompanyUser::withTrashed()
->whereHas("user", function ($query) {
$query->whereColumn("users.account_id", "!=", "company_user.account_id");
})->pluck('id')->implode(",");
$this->logMessage("Cross Linked CompanyUser ids # {$cus}");
} }

View File

@ -264,14 +264,14 @@ class BaseRule implements RuleInterface
return USStates::getState(strlen($this->client->postal_code) > 1 ? $this->client->postal_code : $this->client->shipping_postal_code); return USStates::getState(strlen($this->client->postal_code) > 1 ? $this->client->postal_code : $this->client->shipping_postal_code);
} catch (\Exception $e) { } catch (\Exception $e) {
return $this->client->company->country()->iso_3166_2 == 'US' ? $this->client->company->tax_data->seller_subregion : 'CA'; return 'CA';
} }
} }
public function isTaxableRegion(): bool public function isTaxableRegion(): bool
{ {
return $this->client->company->tax_data->regions->{$this->client_region}->tax_all_subregions || return $this->client->company->tax_data->regions->{$this->client_region}->tax_all_subregions ||
(property_exists($this->client->company->tax_data->regions->{$this->client_region}->subregions, $this->client_subregion) && $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->apply_tax); (property_exists($this->client->company->tax_data->regions->{$this->client_region}->subregions, $this->client_subregion) && ($this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->apply_tax ?? false));
} }
public function defaultForeign(): self public function defaultForeign(): self

View File

@ -36,4 +36,10 @@ interface RuleInterface
public function override($item); public function override($item);
public function calculateRates(); public function calculateRates();
public function regionWithNoTaxCoverage(string $iso_3166_2): bool;
public function setEntity($entity): self;
public function shouldCalcTax(): bool;
} }

View File

@ -55,14 +55,12 @@ class Handler extends ExceptionHandler
protected $selfHostDontReport = [ protected $selfHostDontReport = [
FilePermissionsFailure::class, FilePermissionsFailure::class,
PDOException::class,
MaxAttemptsExceededException::class, MaxAttemptsExceededException::class,
CommandNotFoundException::class, CommandNotFoundException::class,
ValidationException::class, ValidationException::class,
ModelNotFoundException::class, ModelNotFoundException::class,
NotFoundHttpException::class, NotFoundHttpException::class,
UnableToCreateDirectory::class, UnableToCreateDirectory::class,
ConnectException::class,
RuntimeException::class, RuntimeException::class,
InvalidArgumentException::class, InvalidArgumentException::class,
CredentialsException::class, CredentialsException::class,
@ -140,7 +138,7 @@ class Handler extends ExceptionHandler
'email' => 'anonymous@example.com', 'email' => 'anonymous@example.com',
'name' => 'Anonymous User', 'name' => 'Anonymous User',
]); ]);
} elseif (auth()->guard('user') && auth()->guard('user')->user() && auth()->user()->company() && auth()->user()->company()->account->report_errors) { } elseif (auth()->guard('user') && auth()->guard('user')->user() && auth()->user()->companyIsSet() && auth()->user()->company()->account->report_errors) {
$scope->setUser([ $scope->setUser([
'id' => auth()->user()->account->key, 'id' => auth()->user()->account->key,
'email' => 'anonymous@example.com', 'email' => 'anonymous@example.com',

View File

@ -827,7 +827,14 @@ class BaseExport
} }
public function applyFilters(Builder $query): Builder /**
* Apply Product Filters
*
* @param Builder $query
*
* @return Builder
*/
public function applyProductFilters(Builder $query): Builder
{ {
if(isset($this->input['product_key'])) { if(isset($this->input['product_key'])) {
@ -845,7 +852,15 @@ class BaseExport
return $query; return $query;
} }
protected function addClientFilter($query, $clients): Builder /**
* Add Client Filter
*
* @param Builder $query
* @param mixed $clients
*
* @return Builder
*/
protected function addClientFilter(Builder $query, $clients): Builder
{ {
if(is_string($clients)) { if(is_string($clients)) {
$clients = explode(',', $clients); $clients = explode(',', $clients);
@ -863,7 +878,15 @@ class BaseExport
return $query; return $query;
} }
protected function addVendorFilter($query, $vendors): Builder /**
* Add Vendor Filter
*
* @param Builder $query
* @param string $vendors
*
* @return Builder
*/
protected function addVendorFilter(Builder$query, string $vendors): Builder
{ {
if(is_string($vendors)) { if(is_string($vendors)) {
@ -879,7 +902,15 @@ class BaseExport
return $query; return $query;
} }
protected function addProjectFilter($query, $projects): Builder /**
* AddProjectFilter
*
* @param Builder $query
* @param string $projects
*
* @return Builder
*/
protected function addProjectFilter(Builder $query, string $projects): Builder
{ {
if(is_string($projects)) { if(is_string($projects)) {
@ -895,7 +926,15 @@ class BaseExport
return $query; return $query;
} }
protected function addCategoryFilter($query, $expense_categories): Builder /**
* Add Category Filter
*
* @param Builder $query
* @param string $expense_categories
*
* @return Builder
*/
protected function addCategoryFilter(Builder $query, string $expense_categories): Builder
{ {
if(is_string($expense_categories)) { if(is_string($expense_categories)) {
@ -912,12 +951,229 @@ class BaseExport
return $query; return $query;
} }
protected function addInvoiceStatusFilter($query, $status): Builder /**
* Add Payment Status Filters
*
* @param Builder $query
* @param string $status
*
* @return Builder
*/
protected function addPaymentStatusFilters(Builder $query, string $status): Builder
{ {
$status_parameters = explode(',', $status); $status_parameters = explode(',', $status);
if(in_array('all', $status_parameters)) { if(in_array('all', $status_parameters) || count($status_parameters) == 0) {
return $query;
}
$query->where(function ($query) use ($status_parameters) {
$payment_filters = [];
if (in_array('pending', $status_parameters)) {
$payment_filters[] = Payment::STATUS_PENDING;
}
if (in_array('cancelled', $status_parameters)) {
$payment_filters[] = Payment::STATUS_CANCELLED;
}
if (in_array('failed', $status_parameters)) {
$payment_filters[] = Payment::STATUS_FAILED;
}
if (in_array('completed', $status_parameters)) {
$payment_filters[] = Payment::STATUS_COMPLETED;
}
if (in_array('partially_refunded', $status_parameters)) {
$payment_filters[] = Payment::STATUS_PARTIALLY_REFUNDED;
}
if (in_array('refunded', $status_parameters)) {
$payment_filters[] = Payment::STATUS_REFUNDED;
}
if (count($payment_filters) > 0) {
$query->whereIn('status_id', $payment_filters);
}
if(in_array('partially_unapplied', $status_parameters)) {
$query->whereColumn('amount', '>', 'applied')->where('refunded', 0);
}
});
return $query;
}
/**
* Add RecurringInvoice Status Filter
*
* @param Builder $query
* @param string $status
*
* @return Builder
*/
protected function addRecurringInvoiceStatusFilter(Builder $query, string $status): Builder
{
$status_parameters = explode(',', $status);
if (in_array('all', $status_parameters) || count($status_parameters) == 0){
return $query;
}
$recurring_filters = [];
if (in_array('active', $status_parameters)) {
$recurring_filters[] = RecurringInvoice::STATUS_ACTIVE;
}
if (in_array('paused', $status_parameters)) {
$recurring_filters[] = RecurringInvoice::STATUS_PAUSED;
}
if (in_array('completed', $status_parameters)) {
$recurring_filters[] = RecurringInvoice::STATUS_COMPLETED;
}
if (count($recurring_filters) >= 1) {
return $query->whereIn('status_id', $recurring_filters);
}
return $query;
}
/**
* Add QuoteStatus Filter
*
* @param Builder $query
* @param string $status
*
* @return Builder
*/
protected function addQuoteStatusFilter(Builder $query, string $status): Builder
{
$status_parameters = explode(',', $status);
if (in_array('all', $status_parameters)) {
return $query;
}
$query->where(function ($query) use ($status_parameters) {
if (in_array('sent', $status_parameters)) {
$query->orWhere(function ($q) {
$q->where('status_id', Quote::STATUS_SENT)
->whereNull('due_date')
->orWhere('due_date', '>=', now()->toDateString());
});
}
$quote_filters = [];
if (in_array('draft', $status_parameters)) {
$quote_filters[] = Quote::STATUS_DRAFT;
}
if (in_array('approved', $status_parameters)) {
$quote_filters[] = Quote::STATUS_APPROVED;
}
if (count($quote_filters) > 0) {
$query->orWhereIn('status_id', $quote_filters);
}
if (in_array('expired', $status_parameters)) {
$query->orWhere(function ($q) {
$q->where('status_id', Quote::STATUS_SENT)
->whereNotNull('due_date')
->where('due_date', '<=', now()->toDateString());
});
}
if (in_array('upcoming', $status_parameters)) {
$query->orWhere(function ($q) {
$q->where('status_id', Quote::STATUS_SENT)
->where('due_date', '>=', now()->toDateString())
->orderBy('due_date', 'DESC');
});
}
if(in_array('converted', $status_parameters)) {
$query->orWhere(function ($q) {
$q->whereNotNull('invoice_id');
});
}
});
return $query;
}
/**
* Add PurchaseOrder Status Filter
*
* @param Builder $query
* @param string $status
*
* @return Builder
*/
protected function addPurchaseOrderStatusFilter(Builder $query, string $status): Builder
{
$status_parameters = explode(',', $status);
if (in_array('all', $status_parameters) || count($status_parameters) == 0) {
return $query;
}
$query->where(function ($query) use ($status_parameters) {
$po_status = [];
if (in_array('draft', $status_parameters)) {
$po_status[] = PurchaseOrder::STATUS_DRAFT;
}
if (in_array('sent', $status_parameters)) {
$query->orWhere(function ($q) {
$q->where('status_id', PurchaseOrder::STATUS_SENT)
->whereNull('due_date')
->orWhere('due_date', '>=', now()->toDateString());
});
}
if (in_array('accepted', $status_parameters)) {
$po_status[] = PurchaseOrder::STATUS_ACCEPTED;
}
if (in_array('cancelled', $status_parameters)) {
$po_status[] = PurchaseOrder::STATUS_CANCELLED;
}
if (count($po_status) >= 1) {
$query->whereIn('status_id', $po_status);
}
});
return $query;
}
/**
* Add Invoice Status Filter
*
* @param Builder $query
* @param string $status
* @return Builder
*/
protected function addInvoiceStatusFilter(Builder $query, string $status): Builder
{
$status_parameters = explode(',', $status);
if(in_array('all', $status_parameters) || count($status_parameters) == 0) {
return $query; return $query;
} }
@ -942,6 +1198,10 @@ class BaseExport
$invoice_filters[] = Invoice::STATUS_PARTIAL; $invoice_filters[] = Invoice::STATUS_PARTIAL;
} }
if (in_array('cancelled', $status_parameters)) {
$invoice_filters[] = Invoice::STATUS_CANCELLED;
}
if (count($invoice_filters) > 0) { if (count($invoice_filters) > 0) {
$nested->whereIn('status_id', $invoice_filters); $nested->whereIn('status_id', $invoice_filters);
} }
@ -966,14 +1226,18 @@ class BaseExport
return $query; return $query;
} }
protected function addDateRange($query) /**
* Add Date Range
*
* @param Builder $query
* @return Builder
*/
protected function addDateRange(Builder $query): Builder
{ {
$query = $this->applyFilters($query); $query = $this->applyProductFilters($query);
$date_range = $this->input['date_range']; $date_range = $this->input['date_range'];
nlog($date_range);
if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key']) > 1) { if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key']) > 1) {
$this->date_key = $this->input['date_key']; $this->date_key = $this->input['date_key'];
} }

View File

@ -127,7 +127,7 @@ class ClientExport extends BaseExport
$query = Client::query()->with('contacts') $query = Client::query()->with('contacts')
->withTrashed() ->withTrashed()
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);

View File

@ -103,10 +103,14 @@ class CreditExport extends BaseExport
->withTrashed() ->withTrashed()
->with('client') ->with('client')
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);
if($this->input['status'] ?? false) {
$query = $this->addCreditStatusFilter($query, $this->input['status']);
}
if($this->input['document_email_attachment'] ?? false) { if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
} }
@ -162,6 +166,40 @@ class CreditExport extends BaseExport
return $this->decorateAdvancedFields($credit, $entity); return $this->decorateAdvancedFields($credit, $entity);
} }
public function addCreditStatusFilter($query, $status): Builder
{
$status_parameters = explode(',', $status);
if (in_array('all', $status_parameters)) {
return $query;
}
$credit_filters = [];
if (in_array('draft', $status_parameters)) {
$credit_filters[] = Credit::STATUS_DRAFT;
}
if (in_array('sent', $status_parameters)) {
$credit_filters[] = Credit::STATUS_SENT;
}
if (in_array('partial', $status_parameters)) {
$credit_filters[] = Credit::STATUS_PARTIAL;
}
if (in_array('applied', $status_parameters)) {
$credit_filters[] = Credit::STATUS_APPLIED;
}
if (count($credit_filters) >= 1) {
$query->whereIn('status_id', $credit_filters);
}
return $query;
}
private function decorateAdvancedFields(Credit $credit, array $entity): array private function decorateAdvancedFields(Credit $credit, array $entity): array
{ {
// if (in_array('country_id', $this->input['report_keys'])) { // if (in_array('country_id', $this->input['report_keys'])) {

View File

@ -83,10 +83,14 @@ class ExpenseExport extends BaseExport
->with('client') ->with('client')
->withTrashed() ->withTrashed()
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);
if($this->input['status'] ?? false) {
$query = $this->addExpenseStatusFilter($query, $this->input['status']);
}
if(isset($this->input['clients'])) { if(isset($this->input['clients'])) {
$query = $this->addClientFilter($query, $this->input['clients']); $query = $this->addClientFilter($query, $this->input['clients']);
} }
@ -152,6 +156,55 @@ class ExpenseExport extends BaseExport
return $this->decorateAdvancedFields($expense, $entity); return $this->decorateAdvancedFields($expense, $entity);
} }
protected function addExpenseStatusFilter($query, $status): Builder
{
$status_parameters = explode(',', $status);
if (in_array('all', $status_parameters)) {
return $query;
}
$query->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');
});
}
});
return $query;
}
private function decorateAdvancedFields(Expense $expense, array $entity): array private function decorateAdvancedFields(Expense $expense, array $entity): array
{ {
// if (in_array('expense.currency_id', $this->input['report_keys'])) { // if (in_array('expense.currency_id', $this->input['report_keys'])) {

View File

@ -58,7 +58,7 @@ class InvoiceExport extends BaseExport
->withTrashed() ->withTrashed()
->with('client') ->with('client')
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);
@ -151,9 +151,9 @@ class InvoiceExport extends BaseExport
// $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id); // $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id);
// } // }
// if (in_array('invoice.recurring_id', $this->input['report_keys'])) { if (in_array('invoice.recurring_id', $this->input['report_keys'])) {
// $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? ''; $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? '';
// } }
if (in_array('invoice.auto_bill_enabled', $this->input['report_keys'])) { if (in_array('invoice.auto_bill_enabled', $this->input['report_keys'])) {
$entity['invoice.auto_bill_enabled'] = $invoice->auto_bill_enabled ? ctrans('texts.yes') : ctrans('texts.no'); $entity['invoice.auto_bill_enabled'] = $invoice->auto_bill_enabled ? ctrans('texts.yes') : ctrans('texts.no');

View File

@ -71,11 +71,15 @@ class InvoiceItemExport extends BaseExport
->withTrashed() ->withTrashed()
->with('client') ->with('client')
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);
$query = $this->applyFilters($query); if($this->input['status'] ?? false) {
$query = $this->addInvoiceStatusFilter($query, $this->input['status']);
}
$query = $this->applyProductFilters($query);
if($this->input['document_email_attachment'] ?? false) { if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
@ -232,9 +236,9 @@ class InvoiceItemExport extends BaseExport
// $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id); // $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id);
// } // }
// if (in_array('invoice.recurring_id', $this->input['report_keys'])) { if (in_array('invoice.recurring_id', $this->input['report_keys'])) {
// $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? ''; $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? '';
// } }
if (in_array('invoice.assigned_user_id', $this->input['report_keys'])) { if (in_array('invoice.assigned_user_id', $this->input['report_keys'])) {
$entity['invoice.assigned_user_id'] = $invoice->assigned_user ? $invoice->assigned_user->present()->name() : ''; $entity['invoice.assigned_user_id'] = $invoice->assigned_user ? $invoice->assigned_user->present()->name() : '';

View File

@ -61,6 +61,8 @@ class PaymentExport extends BaseExport
$query = $this->addDateRange($query); $query = $this->addDateRange($query);
$query = $this->addPaymentStatusFilters($query, $this->input['status'] ?? '');
if($this->input['document_email_attachment'] ?? false) { if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
} }

View File

@ -31,51 +31,6 @@ class PurchaseOrderExport extends BaseExport
private Decorator $decorator; private Decorator $decorator;
public array $entity_keys = [
'amount' => 'purchase_order.amount',
'balance' => 'purchase_order.balance',
'vendor' => 'purchase_order.vendor_id',
// 'custom_surcharge1' => 'purchase_order.custom_surcharge1',
// 'custom_surcharge2' => 'purchase_order.custom_surcharge2',
// 'custom_surcharge3' => 'purchase_order.custom_surcharge3',
// 'custom_surcharge4' => 'purchase_order.custom_surcharge4',
'custom_value1' => 'purchase_order.custom_value1',
'custom_value2' => 'purchase_order.custom_value2',
'custom_value3' => 'purchase_order.custom_value3',
'custom_value4' => 'purchase_order.custom_value4',
'date' => 'purchase_order.date',
'discount' => 'purchase_order.discount',
'due_date' => 'purchase_order.due_date',
'exchange_rate' => 'purchase_order.exchange_rate',
'footer' => 'purchase_order.footer',
'number' => 'purchase_order.number',
'paid_to_date' => 'purchase_order.paid_to_date',
'partial' => 'purchase_order.partial',
'partial_due_date' => 'purchase_order.partial_due_date',
'po_number' => 'purchase_order.po_number',
'private_notes' => 'purchase_order.private_notes',
'public_notes' => 'purchase_order.public_notes',
'status' => 'purchase_order.status',
'tax_name1' => 'purchase_order.tax_name1',
'tax_name2' => 'purchase_order.tax_name2',
'tax_name3' => 'purchase_order.tax_name3',
'tax_rate1' => 'purchase_order.tax_rate1',
'tax_rate2' => 'purchase_order.tax_rate2',
'tax_rate3' => 'purchase_order.tax_rate3',
'terms' => 'purchase_order.terms',
'total_taxes' => 'purchase_order.total_taxes',
'currency_id' => 'purchase_order.currency_id',
];
private array $decorate_keys = [
'country',
'currency_id',
'status',
'vendor',
'project',
];
public function __construct(Company $company, array $input) public function __construct(Company $company, array $input)
{ {
$this->company = $company; $this->company = $company;
@ -104,10 +59,12 @@ class PurchaseOrderExport extends BaseExport
->withTrashed() ->withTrashed()
->with('vendor') ->with('vendor')
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);
$query = $this->addPurchaseOrderStatusFilter($query, $this->input['status'] ?? '');
if($this->input['document_email_attachment'] ?? false) { if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
} }
@ -167,7 +124,7 @@ class PurchaseOrderExport extends BaseExport
if (is_array($parts) && $parts[0] == 'purchase_order' && array_key_exists($parts[1], $transformed_purchase_order)) { if (is_array($parts) && $parts[0] == 'purchase_order' && array_key_exists($parts[1], $transformed_purchase_order)) {
$entity[$key] = $transformed_purchase_order[$parts[1]]; $entity[$key] = $transformed_purchase_order[$parts[1]];
} else { } else {
// nlog($key); nlog($key);
$entity[$key] = $this->decorator->transform($key, $purchase_order); $entity[$key] = $this->decorator->transform($key, $purchase_order);
// $entity[$key] = ''; // $entity[$key] = '';
@ -182,16 +139,13 @@ class PurchaseOrderExport extends BaseExport
private function decorateAdvancedFields(PurchaseOrder $purchase_order, array $entity): array private function decorateAdvancedFields(PurchaseOrder $purchase_order, array $entity): array
{ {
if (in_array('country_id', $this->input['report_keys'])) {
$entity['country'] = $purchase_order->vendor->country ? ctrans("texts.country_{$purchase_order->vendor->country->name}") : ''; if (in_array('purchase_order.currency_id', $this->input['report_keys'])) {
$entity['purchase_order.currency_id'] = $purchase_order->vendor->currency() ? $purchase_order->vendor->currency()->code : $purchase_order->company->currency()->code;
} }
if (in_array('currency_id', $this->input['report_keys'])) { if (in_array('purchase_order.vendor_id', $this->input['report_keys'])) {
$entity['currency_id'] = $purchase_order->vendor->currency() ? $purchase_order->vendor->currency()->code : $purchase_order->company->currency()->code; $entity['purchase_order.vendor_id'] = $purchase_order->vendor->present()->name();
}
if (in_array('vendor_id', $this->input['report_keys'])) {
$entity['vendor'] = $purchase_order->vendor->present()->name();
} }
if (in_array('purchase_order.status', $this->input['report_keys'])) { if (in_array('purchase_order.status', $this->input['report_keys'])) {

View File

@ -63,10 +63,12 @@ class PurchaseOrderItemExport extends BaseExport
$query = PurchaseOrder::query() $query = PurchaseOrder::query()
->withTrashed() ->withTrashed()
->with('vendor')->where('company_id', $this->company->id) ->with('vendor')->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);
$query = $this->addPurchaseOrderStatusFilter($query, $this->input['status'] ?? '');
if($this->input['document_email_attachment'] ?? false) { if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
} }
@ -190,23 +192,35 @@ class PurchaseOrderItemExport extends BaseExport
private function decorateAdvancedFields(PurchaseOrder $purchase_order, array $entity): array private function decorateAdvancedFields(PurchaseOrder $purchase_order, array $entity): array
{ {
if (in_array('currency_id', $this->input['report_keys'])) { // if (in_array('currency_id', $this->input['report_keys'])) {
$entity['currency'] = $purchase_order->vendor->currency() ? $purchase_order->vendor->currency()->code : $purchase_order->company->currency()->code; // $entity['currency'] = $purchase_order->vendor->currency() ? $purchase_order->vendor->currency()->code : $purchase_order->company->currency()->code;
// }
// if(array_key_exists('type', $entity)) {
// $entity['type'] = $purchase_order->typeIdString($entity['type']);
// }
// if(array_key_exists('tax_category', $entity)) {
// $entity['tax_category'] = $purchase_order->taxTypeString($entity['tax_category']);
// }
// if($this->force_keys) {
// $entity['vendor'] = $purchase_order->vendor->present()->name();
// $entity['vendor_id_number'] = $purchase_order->vendor->id_number;
// $entity['vendor_number'] = $purchase_order->vendor->number;
// $entity['status'] = $purchase_order->stringStatus($purchase_order->status_id);
// }
if (in_array('purchase_order.currency_id', $this->input['report_keys'])) {
$entity['purchase_order.currency_id'] = $purchase_order->vendor->currency() ? $purchase_order->vendor->currency()->code : $purchase_order->company->currency()->code;
} }
if(array_key_exists('type', $entity)) { if (in_array('purchase_order.vendor_id', $this->input['report_keys'])) {
$entity['type'] = $purchase_order->typeIdString($entity['type']); $entity['purchase_order.vendor_id'] = $purchase_order->vendor->present()->name();
} }
if(array_key_exists('tax_category', $entity)) { if (in_array('purchase_order.status', $this->input['report_keys'])) {
$entity['tax_category'] = $purchase_order->taxTypeString($entity['tax_category']); $entity['purchase_order.status'] = $purchase_order->stringStatus($purchase_order->status_id);
}
if($this->force_keys) {
$entity['vendor'] = $purchase_order->vendor->present()->name();
$entity['vendor_id_number'] = $purchase_order->vendor->id_number;
$entity['vendor_number'] = $purchase_order->vendor->number;
$entity['status'] = $purchase_order->stringStatus($purchase_order->status_id);
} }
if (in_array('purchase_order.user_id', $this->input['report_keys'])) { if (in_array('purchase_order.user_id', $this->input['report_keys'])) {

View File

@ -65,10 +65,12 @@ class QuoteExport extends BaseExport
->withTrashed() ->withTrashed()
->with('client') ->with('client')
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);
$query = $this->addQuoteStatusFilter($query, $this->input['status'] ?? '');
if($this->input['document_email_attachment'] ?? false) { if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
} }

View File

@ -66,10 +66,12 @@ class QuoteItemExport extends BaseExport
$query = Quote::query() $query = Quote::query()
->withTrashed() ->withTrashed()
->with('client')->where('company_id', $this->company->id) ->with('client')->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);
$query = $this->addQuoteStatusFilter($query, $this->input['status'] ?? '');
if($this->input['document_email_attachment'] ?? false) { if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query); $this->queueDocuments($query);
} }

View File

@ -57,10 +57,12 @@ class RecurringInvoiceExport extends BaseExport
->withTrashed() ->withTrashed()
->with('client') ->with('client')
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);
$query = $this->addRecurringInvoiceStatusFilter($query, $this->input['status'] ?? '');
return $query; return $query;
} }

View File

@ -68,7 +68,7 @@ class TaskExport extends BaseExport
$query = Task::query() $query = Task::query()
->withTrashed() ->withTrashed()
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);
@ -203,6 +203,34 @@ class TaskExport extends BaseExport
} }
/**
* Add Task Status Filter
*
* @param Builder $query
* @param string $status
* @return Builder
*/
protected function addTaskStatusFilter(Builder $query, string $status): Builder
{
$status_parameters = explode(',', $status);
if (in_array('all', $status_parameters) || count($status_parameters) == 0) {
return $query;
}
if (in_array('invoiced', $status_parameters)) {
$query->whereNotNull('invoice_id');
}
if (in_array('uninvoiced', $status_parameters)) {
$query->whereNull('invoice_id');
}
return $query;
}
private function decorateAdvancedFields(Task $task, array $entity): array private function decorateAdvancedFields(Task $task, array $entity): array
{ {
if (in_array('task.status_id', $this->input['report_keys'])) { if (in_array('task.status_id', $this->input['report_keys'])) {

View File

@ -62,7 +62,7 @@ class VendorExport extends BaseExport
$query = Vendor::query()->with('contacts') $query = Vendor::query()->with('contacts')
->withTrashed() ->withTrashed()
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);

View File

@ -79,7 +79,6 @@ class InvoiceDecorator extends Decorator implements DecoratorInterface
return $invoice->partial_due_date ?? ''; return $invoice->partial_due_date ?? '';
} }
public function assigned_user_id(Invoice $invoice) public function assigned_user_id(Invoice $invoice)
{ {
return $invoice->assigned_user ? $invoice->assigned_user->present()->name() : ''; return $invoice->assigned_user ? $invoice->assigned_user->present()->name() : '';

View File

@ -29,11 +29,13 @@ class DocumentFilters extends QueryFilters
*/ */
public function filter(string $filter = ''): Builder public function filter(string $filter = ''): Builder
{ {
if (strlen($filter) == 0) { if (strlen($filter) == 0) {
return $this->builder; return $this->builder;
} }
return $this->builder; return $this->builder->where('name', 'like', '%'.$filter.'%');
} }
/** /**
@ -47,6 +49,43 @@ class DocumentFilters extends QueryFilters
*/ */
public function client_id(string $client_id = ''): Builder public function client_id(string $client_id = ''): Builder
{ {
return $this->builder->where(function ($query) use ($client_id) {
$query->whereHasMorph('documentable', [
\App\Models\Invoice::class,
\App\Models\Quote::class,
\App\Models\Credit::class,
\App\Models\Expense::class,
\App\Models\Payment::class,
\App\Models\Task::class,
\App\Models\RecurringExpense::class,
\App\Models\RecurringInvoice::class,
\App\Models\Project::class,
], function ($q2) use ($client_id) {
$q2->where('client_id', $this->decodePrimaryKey($client_id));
})->orWhereHasMorph('documentable', [\App\Models\Client::class], function ($q3) use ($client_id) {
$q3->where('id', $this->decodePrimaryKey($client_id));
});
});
}
public function type(string $types = '')
{
$types = explode(',', $types);
foreach ($types as $type)
{
match($type) {
'private' => $this->builder->where('is_public', 0),
'public' => $this->builder->where('is_public', 1),
'pdf' => $this->builder->where('type', 'pdf'),
'image' => $this->builder->whereIn('type', ['png','jpeg','jpg','gif','svg']),
'other' => $this->builder->whereNotIn('type', ['pdf','png','jpeg','jpg','gif','svg']),
default => $this->builder,
};
}
return $this->builder; return $this->builder;
} }

View File

@ -62,6 +62,10 @@ class InvoiceFilters extends QueryFilters
$invoice_filters[] = Invoice::STATUS_PAID; $invoice_filters[] = Invoice::STATUS_PAID;
} }
if (in_array('cancelled', $status_parameters)) {
$invoice_filters[] = Invoice::STATUS_CANCELLED;
}
if (in_array('unpaid', $status_parameters)) { if (in_array('unpaid', $status_parameters)) {
$invoice_filters[] = Invoice::STATUS_SENT; $invoice_filters[] = Invoice::STATUS_SENT;
$invoice_filters[] = Invoice::STATUS_PARTIAL; $invoice_filters[] = Invoice::STATUS_PARTIAL;
@ -324,6 +328,7 @@ class InvoiceFilters extends QueryFilters
} }
if($sort_col[0] == 'number') { if($sort_col[0] == 'number') {
// return $this->builder->orderByRaw('CAST(number AS UNSIGNED), number ' . $dir);
return $this->builder->orderByRaw('ABS(number) ' . $dir); return $this->builder->orderByRaw('ABS(number) ' . $dir);
} }

View File

@ -164,7 +164,7 @@ class PaymentFilters extends QueryFilters
{ {
$sort_col = explode('|', $sort); $sort_col = explode('|', $sort);
if (!is_array($sort_col) || count($sort_col) != 2 || !in_array($sort_col, Schema::getColumnListing('payments'))) { if (!is_array($sort_col) || count($sort_col) != 2 || !in_array($sort_col[0], Schema::getColumnListing('payments'))) {
return $this->builder; return $this->builder;
} }

View File

@ -50,7 +50,7 @@ class UserFilters extends QueryFilters
{ {
$sort_col = explode('|', $sort); $sort_col = explode('|', $sort);
if (!is_array($sort_col) || count($sort_col) != 2) { if (!is_array($sort_col) || count($sort_col) != 2 || !in_array($sort_col[0], \Illuminate\Support\Facades\Schema::getColumnListing('users'))) {
return $this->builder; return $this->builder;
} }

View File

@ -19,6 +19,7 @@
namespace App\Helpers\Bank\Nordigen; namespace App\Helpers\Bank\Nordigen;
use App\Models\Company;
use App\Services\Email\Email; use App\Services\Email\Email;
use App\Models\BankIntegration; use App\Models\BankIntegration;
use App\Services\Email\EmailObject; use App\Services\Email\EmailObject;
@ -96,11 +97,11 @@ class Nordigen
return $it->transform($out); return $it->transform($out);
} catch (\Exception $e) { } catch (\Exception $e) {
if (strpos($e->getMessage(), "Invalid Account ID") !== false) {
return false;
}
throw $e; nlog("Nordigen getAccount() failed => {$account_id} => " . $e->getMessage());
return false;
} }
} }
@ -138,11 +139,11 @@ class Nordigen
* @param string $dateFrom * @param string $dateFrom
* @return array * @return array
*/ */
public function getTransactions(string $accountId, string $dateFrom = null): array public function getTransactions(Company $company, string $accountId, string $dateFrom = null): array
{ {
$transactionResponse = $this->client->account($accountId)->getAccountTransactions($dateFrom); $transactionResponse = $this->client->account($accountId)->getAccountTransactions($dateFrom);
$it = new TransactionTransformer(); $it = new TransactionTransformer($company);
return $it->transform($transactionResponse); return $it->transform($transactionResponse);
} }

View File

@ -12,7 +12,10 @@
namespace App\Helpers\Bank\Nordigen\Transformer; namespace App\Helpers\Bank\Nordigen\Transformer;
use App\Helpers\Bank\BankRevenueInterface; use App\Helpers\Bank\BankRevenueInterface;
use App\Models\BankIntegration; use App\Models\Company;
use App\Models\DateFormat;
use App\Models\Timezone;
use Carbon\Carbon;
use App\Utils\Traits\AppSetup; use App\Utils\Traits\AppSetup;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Log; use Log;
@ -66,6 +69,13 @@ class TransactionTransformer implements BankRevenueInterface
{ {
use AppSetup; use AppSetup;
private Company $company;
function __construct(Company $company)
{
$this->company = $company;
}
public function transform($transactionResponse) public function transform($transactionResponse)
{ {
$data = []; $data = [];
@ -112,8 +122,8 @@ class TransactionTransformer implements BankRevenueInterface
// enrich description with currencyExchange informations // enrich description with currencyExchange informations
if (isset($transaction['currencyExchange'])) { if (isset($transaction['currencyExchange'])) {
foreach ($transaction["currencyExchange"] as $exchangeRate) { foreach ($transaction["currencyExchange"] as $exchangeRate) {
$targetAmount = round($amount * (float) ($exchangeRate["exchangeRate"] ?? 1) , 2); $targetAmount = round($amount * (float) ($exchangeRate["exchangeRate"] ?? 1), 2);
$description .= '\nexchangeRate: ' . $amount . " " . ($exchangeRate["sourceCurrency"] ?? '?') . " = " . $targetAmount . " " . ($exchangeRate["targetCurrency"] ?? '?') . " (" . ($exchangeRate["quotationDate"] ?? '?') . ")"; $description .= '\n' . ctrans('texts.exchange_rate') . ' : ' . $amount . " " . ($exchangeRate["sourceCurrency"] ?? '?') . " = " . $targetAmount . " " . ($exchangeRate["targetCurrency"] ?? '?') . " (" . (isset($exchangeRate["quotationDate"]) ? $this->formatDate($exchangeRate["quotationDate"]) : '?') . ")";
} }
} }
@ -164,4 +174,24 @@ class TransactionTransformer implements BankRevenueInterface
} }
private function formatDate(string $input)
{
$timezone = Timezone::find($this->company->settings->timezone_id);
$timezone_name = 'US/Eastern';
if ($timezone) {
$timezone_name = $timezone->name;
}
$date_format_default = 'Y-m-d';
$date_format = DateFormat::find($this->company->settings->date_format_id);
if ($date_format) {
$date_format_default = $date_format->format;
}
return Carbon::createFromFormat("d-m-Y", $input)->setTimezone($timezone_name)->format($date_format_default) ?? $input;
}
} }

View File

@ -177,6 +177,8 @@ class InvoiceItemSum
if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions
/** @var \App\DataMapper\Tax\BaseRule $class */
$class = "App\DataMapper\Tax\\".$this->client->company->country()->iso_3166_2."\\Rule"; $class = "App\DataMapper\Tax\\".$this->client->company->country()->iso_3166_2."\\Rule";
$this->rule = new $class(); $this->rule = new $class();

View File

@ -122,7 +122,7 @@ class InvoiceSum
private function calculateInvoiceTaxes(): self private function calculateInvoiceTaxes(): self
{ {
if (is_string($this->invoice->tax_name1) && strlen($this->invoice->tax_name1) > 1) { if (is_string($this->invoice->tax_name1) && strlen($this->invoice->tax_name1) >= 2) {
$tax = $this->taxer($this->total, $this->invoice->tax_rate1); $tax = $this->taxer($this->total, $this->invoice->tax_rate1);
$tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name1, $this->invoice->tax_rate1); $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name1, $this->invoice->tax_rate1);
@ -130,7 +130,7 @@ class InvoiceSum
$this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax];
} }
if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) > 1) { if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) >= 2) {
$tax = $this->taxer($this->total, $this->invoice->tax_rate2); $tax = $this->taxer($this->total, $this->invoice->tax_rate2);
$tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name2, $this->invoice->tax_rate2); $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name2, $this->invoice->tax_rate2);
@ -138,7 +138,7 @@ class InvoiceSum
$this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax];
} }
if (is_string($this->invoice->tax_name3) && strlen($this->invoice->tax_name3) > 1) { if (is_string($this->invoice->tax_name3) && strlen($this->invoice->tax_name3) >= 2) {
$tax = $this->taxer($this->total, $this->invoice->tax_rate3); $tax = $this->taxer($this->total, $this->invoice->tax_rate3);
$tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name3, $this->invoice->tax_rate3); $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name3, $this->invoice->tax_rate3);
@ -242,9 +242,9 @@ class InvoiceSum
if ($this->invoice->status_id != Invoice::STATUS_DRAFT) { if ($this->invoice->status_id != Invoice::STATUS_DRAFT) {
if ($this->invoice->amount != $this->invoice->balance) { if ($this->invoice->amount != $this->invoice->balance) {
$paid_to_date = $this->invoice->amount - $this->invoice->balance; // $paid_to_date = $this->invoice->amount - $this->invoice->balance;
$this->invoice->balance = Number::roundValue($this->getTotal(), $this->precision) - $paid_to_date; $this->invoice->balance = Number::roundValue($this->getTotal(), $this->precision) - $this->invoice->paid_to_date; //21-02-2024 cannot use the calculated $paid_to_date here as it could send the balance backward.
} else { } else {
$this->invoice->balance = Number::roundValue($this->getTotal(), $this->precision); $this->invoice->balance = Number::roundValue($this->getTotal(), $this->precision);
} }

View File

@ -131,20 +131,20 @@ class InvoiceSumInclusive
$amount = $this->formatValue(($this->sub_total - ($this->sub_total * ($this->invoice->discount / 100))), 2); $amount = $this->formatValue(($this->sub_total - ($this->sub_total * ($this->invoice->discount / 100))), 2);
} }
if ($this->invoice->tax_rate1 > 0) { if (is_string($this->invoice->tax_name1) && strlen($this->invoice->tax_name1) > 1) {
$tax = $this->calcInclusiveLineTax($this->invoice->tax_rate1, $amount); $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate1, $amount);
$this->total_taxes += $tax; $this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax];
} }
if ($this->invoice->tax_rate2 > 0) { if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) > 1) {
$tax = $this->calcInclusiveLineTax($this->invoice->tax_rate2, $amount); $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate2, $amount);
$this->total_taxes += $tax; $this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax];
} }
if ($this->invoice->tax_rate3 > 0) { if (is_string($this->invoice->tax_name3) && strlen($this->invoice->tax_name3) > 1) {
$tax = $this->calcInclusiveLineTax($this->invoice->tax_rate3, $amount); $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate3, $amount);
$this->total_taxes += $tax; $this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax];
@ -259,9 +259,9 @@ class InvoiceSumInclusive
/* If amount != balance then some money has been paid on the invoice, need to subtract this difference from the total to set the new balance */ /* If amount != balance then some money has been paid on the invoice, need to subtract this difference from the total to set the new balance */
if ($this->invoice->status_id != Invoice::STATUS_DRAFT) { if ($this->invoice->status_id != Invoice::STATUS_DRAFT) {
if ($this->invoice->amount != $this->invoice->balance) { if ($this->invoice->amount != $this->invoice->balance) {
$paid_to_date = $this->invoice->amount - $this->invoice->balance; // $paid_to_date = $this->invoice->amount - $this->invoice->balance;
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision) - $paid_to_date; $this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision) - $this->invoice->paid_to_date;
} else { } else {
$this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision); $this->invoice->balance = $this->formatValue($this->getTotal(), $this->precision);
} }

View File

@ -159,7 +159,7 @@ class SwissQrGenerator
// Optionally, add some human-readable information about what the bill is for. // Optionally, add some human-readable information about what the bill is for.
$qrBill->setAdditionalInformation( $qrBill->setAdditionalInformation(
QrBill\DataGroup\Element\AdditionalInformation::create( QrBill\DataGroup\Element\AdditionalInformation::create(
$this->invoice->public_notes ? substr($this->invoice->public_notes, 0, 139) : ctrans('texts.invoice_number_placeholder', ['invoice' => $this->invoice->number]) $this->invoice->public_notes ? substr(strip_tags($this->invoice->public_notes), 0, 139) : ctrans('texts.invoice_number_placeholder', ['invoice' => $this->invoice->number])
) )
); );

View File

@ -49,6 +49,9 @@ use App\Http\Requests\Client\ClientDocumentsRequest;
use App\Http\Requests\Client\ReactivateClientEmailRequest; use App\Http\Requests\Client\ReactivateClientEmailRequest;
use App\Models\Expense; use App\Models\Expense;
use App\Models\Payment; use App\Models\Payment;
use App\Models\Project;
use App\Models\RecurringExpense;
use App\Models\RecurringInvoice;
use App\Models\Task; use App\Models\Task;
use App\Transformers\DocumentTransformer; use App\Transformers\DocumentTransformer;
@ -421,7 +424,7 @@ class ClientController extends BaseController
$documents = Document::query() $documents = Document::query()
->company() ->company()
->whereHasMorph('documentable', [Invoice::class, Quote::class, Credit::class, Expense::class, Payment::class, Task::class], function ($query) use ($client) { ->whereHasMorph('documentable', [Invoice::class, Quote::class, Credit::class, Expense::class, Payment::class, Task::class, RecurringInvoice::class, RecurringExpense::class, Project::class], function ($query) use ($client) {
$query->where('client_id', $client->id); $query->where('client_id', $client->id);
}) })
->orWhereHasMorph('documentable', [Client::class], function ($query) use ($client) { ->orWhereHasMorph('documentable', [Client::class], function ($query) use ($client) {

View File

@ -45,7 +45,7 @@ class EmailPreferencesController extends Controller
if ($invitation->contact->is_locked && !Cache::has("unsubscribe_notitfication_suppression:{$invitation_key}")) { if ($invitation->contact->is_locked && !Cache::has("unsubscribe_notitfication_suppression:{$invitation_key}")) {
$nmo = new NinjaMailerObject(); $nmo = new NinjaMailerObject();
$nmo->mailable = new NinjaMailer((new ClientUnsubscribedObject($invitation->contact, $invitation->contact->company, $invitation->contact->company->owner()->company_users()->first()->portalType() ?? true))->build()); $nmo->mailable = new NinjaMailer((new ClientUnsubscribedObject($invitation->contact, $invitation->contact->company, true))->build());
$nmo->company = $invitation->contact->company; $nmo->company = $invitation->contact->company;
$nmo->to_user = $invitation->contact->company->owner(); $nmo->to_user = $invitation->contact->company->owner();
$nmo->settings = $invitation->contact->company->settings; $nmo->settings = $invitation->contact->company->settings;

View File

@ -271,6 +271,7 @@ class InvitationController extends Controller
->with('contact.client') ->with('contact.client')
->firstOrFail(); ->firstOrFail();
if ($invitation->contact->trashed()) { if ($invitation->contact->trashed()) {
$invitation->contact->restore(); $invitation->contact->restore();
} }
@ -294,7 +295,10 @@ class InvitationController extends Controller
'payable_invoices' => [ 'payable_invoices' => [
['invoice_id' => $invitation->invoice->hashed_id, 'amount' => $amount], ['invoice_id' => $invitation->invoice->hashed_id, 'amount' => $amount],
], ],
'signature' => false 'signature' => false,
'contact_first_name' => $invitation->contact->first_name ?? '',
'contact_last_name' => $invitation->contact->last_name ?? '',
'contact_email' => $invitation->contact->email ?? ''
]; ];
$request->replace($data); $request->replace($data);

View File

@ -236,7 +236,6 @@ class InvoiceController extends Controller
'hashed_ids' => $invoices->pluck('hashed_id'), 'hashed_ids' => $invoices->pluck('hashed_id'),
'total' => $total, 'total' => $total,
'variables' => $variables, 'variables' => $variables,
]; ];
return $this->render('invoices.payment', $data); return $this->render('invoices.payment', $data);

View File

@ -12,16 +12,17 @@
namespace App\Http\Controllers\ClientPortal; namespace App\Http\Controllers\ClientPortal;
use App\Utils\Number;
use App\Utils\HtmlEngine;
use Illuminate\View\View;
use App\DataMapper\InvoiceItem; use App\DataMapper\InvoiceItem;
use App\Factory\InvoiceFactory; use App\Factory\InvoiceFactory;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\PrePayments\StorePrePaymentRequest;
use App\Repositories\InvoiceRepository;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesDates;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
use Illuminate\View\View; use App\Repositories\InvoiceRepository;
use App\Http\Requests\ClientPortal\PrePayments\StorePrePaymentRequest;
/** /**
* Class PrePaymentController. * Class PrePaymentController.
@ -88,6 +89,8 @@ class PrePaymentController extends Controller
$total = $invoice->balance; $total = $invoice->balance;
$invitation = $invoice->invitations->first();
//format totals //format totals
$formatted_total = Number::formatMoney($invoice->amount, auth()->guard('contact')->user()->client); $formatted_total = Number::formatMoney($invoice->amount, auth()->guard('contact')->user()->client);
@ -113,6 +116,8 @@ class PrePaymentController extends Controller
'frequency_id' => $request->frequency_id, 'frequency_id' => $request->frequency_id,
'remaining_cycles' => $request->remaining_cycles, 'remaining_cycles' => $request->remaining_cycles,
'is_recurring' => $request->is_recurring == 'on' ? true : false, 'is_recurring' => $request->is_recurring == 'on' ? true : false,
'variables' => $variables = ($invitation && auth()->guard('contact')->user()->client->getSetting('show_accept_invoice_terms')) ? (new HtmlEngine($invitation))->generateLabelsAndValues() : false,
]; ];
return $this->render('invoices.payment', $data); return $this->render('invoices.payment', $data);

View File

@ -25,6 +25,10 @@ class SubscriptionPurchaseController extends Controller
{ {
App::setLocale($subscription->company->locale()); App::setLocale($subscription->company->locale());
if ($subscription->trashed()) {
return $this->render('generic.not_available', ['account' => $subscription->company->account, 'company' => $subscription->company]);
}
/* Make sure the contact is logged into the correct company for this subscription */ /* Make sure the contact is logged into the correct company for this subscription */
if (auth()->guard('contact')->user() && auth()->guard('contact')->user()->company_id != $subscription->company_id) { if (auth()->guard('contact')->user() && auth()->guard('contact')->user()->company_id != $subscription->company_id) {
auth()->guard('contact')->logout(); auth()->guard('contact')->logout();

View File

@ -260,6 +260,8 @@ class ImportController extends Controller
} }
} }
/** @phpstan-ignore-next-line **/
return $bestDelimiter ?? ','; return $bestDelimiter ?? ',';
} }

View File

@ -158,7 +158,6 @@ class LicenseController extends BaseController
/* Catch claim license requests */ /* Catch claim license requests */
if (config('ninja.environment') == 'selfhost') { if (config('ninja.environment') == 'selfhost') {
// $response = Http::get( "http://ninja.test:8000/claim_license", [
$response = Http::get("https://invoicing.co/claim_license", [ $response = Http::get("https://invoicing.co/claim_license", [
'license_key' => $license_key, 'license_key' => $license_key,
'product_id' => 3, 'product_id' => 3,

View File

@ -21,7 +21,7 @@ class ProtectedDownloadController extends BaseController
public function index(Request $request, string $hash) public function index(Request $request, string $hash)
{ {
/** @var string $hashed_path */ /** @var string $hashed_path */
$hashed_path = Cache::pull($hash); $hashed_path = Cache::get($hash);
if (!$hashed_path) { if (!$hashed_path) {
throw new SystemError('File no longer available', 404); throw new SystemError('File no longer available', 404);

View File

@ -30,18 +30,24 @@ class SmtpController extends BaseController
$user = auth()->user(); $user = auth()->user();
$company = $user->company(); $company = $user->company();
$smtp_host = $request->input('smtp_host', $company->smtp_host);
$smtp_port = $request->input('smtp_port', $company->smtp_port);
$smtp_username = $request->input('smtp_username', $company->smtp_username);
$smtp_password = $request->input('smtp_password', $company->smtp_password);
$smtp_encryption = $request->input('smtp_encryption', $company->smtp_encryption ?? 'tls');
$smtp_local_domain = $request->input('smtp_local_domain', strlen($company->smtp_local_domain) > 2 ? $company->smtp_local_domain : null);
$smtp_verify_peer = $request->input('verify_peer', $company->smtp_verify_peer ?? true);
config([ config([
'mail.mailers.smtp' => [ 'mail.mailers.smtp' => [
'transport' => 'smtp', 'transport' => 'smtp',
'host' => $request->input('smtp_host', $company->smtp_host), 'host' => $smtp_host,
'port' => $request->input('smtp_port', $company->smtp_port), 'port' => $smtp_port,
'username' => $request->input('smtp_username', $company->smtp_username), 'username' => $smtp_username,
'password' => $request->input('smtp_password', $company->smtp_password), 'password' => $smtp_password,
'encryption' => $request->input('smtp_encryption', $company->smtp_encryption ?? 'tls'), 'encryption' => $smtp_encryption,
'local_domain' => $request->input('smtp_local_domain', strlen($company->smtp_local_domain) > 2 ? $company->smtp_local_domain : null), 'local_domain' => $smtp_local_domain,
'verify_peer' => $request->input('verify_peer', $company->smtp_verify_peer ?? true), 'verify_peer' => $smtp_verify_peer,
'timeout' => 5, 'timeout' => 5,
], ],
]); ]);
@ -49,7 +55,7 @@ class SmtpController extends BaseController
(new \Illuminate\Mail\MailServiceProvider(app()))->register(); (new \Illuminate\Mail\MailServiceProvider(app()))->register();
try { try {
Mail::to($user->email, $user->present()->name())->send(new TestMailServer('Email Server Works!', strlen($company->settings->custom_sending_email) > 1 ? $company->settings->custom_sending_email : $user->email)); Mail::mailer('smtp')->to($user->email, $user->present()->name())->send(new TestMailServer('Email Server Works!', strlen($company->settings->custom_sending_email) > 1 ? $company->settings->custom_sending_email : $user->email));
} catch (\Exception $e) { } catch (\Exception $e) {
app('mail.manager')->forgetMailers(); app('mail.manager')->forgetMailers();
return response()->json(['message' => $e->getMessage()], 400); return response()->json(['message' => $e->getMessage()], 400);

View File

@ -65,6 +65,8 @@ class StripeConnectController extends BaseController
return view('auth.connect.access_denied'); return view('auth.connect.access_denied');
} }
$response = false;
try { try {
/** @class \stdClass $response /** @class \stdClass $response
* @property string $scope * @property string $scope
@ -88,6 +90,11 @@ class StripeConnectController extends BaseController
nlog($response); nlog($response);
} catch (\Exception $e) { } catch (\Exception $e) {
}
if(!$response) {
return view('auth.connect.access_denied'); return view('auth.connect.access_denied');
} }
@ -144,11 +151,14 @@ class StripeConnectController extends BaseController
if(isset($request->getTokenContent()['is_react']) && $request->getTokenContent()['is_react']) { if(isset($request->getTokenContent()['is_react']) && $request->getTokenContent()['is_react']) {
$redirect_uri = config('ninja.react_url').'/#/settings/online_payments'; $redirect_uri = config('ninja.react_url').'/#/settings/online_payments';
} else { } else {
$redirect_uri = config('ninja.app_url').'/stripe/completed'; $redirect_uri = config('ninja.app_url');
} }
\Illuminate\Support\Facades\Cache::pull($request->token);
//response here //response here
return view('auth.connect.completed', ['url' => $redirect_uri]); return view('auth.connect.completed', ['url' => $redirect_uri]);
// return redirect($redirect_uri);
} }
} }

View File

@ -72,6 +72,10 @@ class TwoFactorController extends BaseController
return response()->json(['message' => ctrans('texts.enabled_two_factor')], 200); return response()->json(['message' => ctrans('texts.enabled_two_factor')], 200);
} elseif (! $secret || ! $google2fa->verifyKey($secret, $oneTimePassword)) { } elseif (! $secret || ! $google2fa->verifyKey($secret, $oneTimePassword)) {
return response()->json(['message' => ctrans('texts.invalid_one_time_password')], 400); return response()->json(['message' => ctrans('texts.invalid_one_time_password')], 400);
}elseif (! $user->phone) {
return response()->json(['message' => ctrans('texts.set_phone_for_two_factor')], 400);
} elseif (! $user->isVerified()) {
return response()->json(['message' => 'Please confirm your account first'], 400);
} }
return response()->json(['message' => 'No phone record or user is not confirmed'], 400); return response()->json(['message' => 'No phone record or user is not confirmed'], 400);

View File

@ -135,7 +135,7 @@ class Kernel extends HttpKernel
'can' => Authorize::class, 'can' => Authorize::class,
'cors' => Cors::class, 'cors' => Cors::class,
'guest' => RedirectIfAuthenticated::class, 'guest' => RedirectIfAuthenticated::class,
'signed' => ValidateSignature::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'verified' => EnsureEmailIsVerified::class, 'verified' => EnsureEmailIsVerified::class,
'query_logging' => QueryLogging::class, 'query_logging' => QueryLogging::class,
'token_auth' => TokenAuth::class, 'token_auth' => TokenAuth::class,

View File

@ -59,20 +59,6 @@ class StoreClientRequest extends Request
$rules['file'] = $this->file_validation; $rules['file'] = $this->file_validation;
} }
if (isset($this->number)) {
$rules['number'] = Rule::unique('clients')->where('company_id', $user->company()->id);
}
$rules['country_id'] = 'integer|nullable';
if (isset($this->currency_code)) {
$rules['currency_code'] = 'sometimes|exists:currencies,code';
}
if (isset($this->country_code)) {
$rules['country_code'] = new CountryCodeExistsRule();
}
/* Ensure we have a client name, and that all emails are unique*/ /* Ensure we have a client name, and that all emails are unique*/
//$rules['name'] = 'required|min:1'; //$rules['name'] = 'required|min:1';
$rules['settings'] = new ValidClientGroupSettingsRule(); $rules['settings'] = new ValidClientGroupSettingsRule();
@ -97,6 +83,9 @@ class StoreClientRequest extends Request
$rules['number'] = ['bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)]; $rules['number'] = ['bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)];
$rules['id_number'] = ['bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)]; $rules['id_number'] = ['bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)];
$rules['classification'] = 'bail|sometimes|nullable|in:individual,business,company,partnership,trust,charity,government,other'; $rules['classification'] = 'bail|sometimes|nullable|in:individual,business,company,partnership,trust,charity,government,other';
$rules['shipping_country_id'] = 'integer|nullable|exists:countries,id';
$rules['number'] = ['sometimes', 'nullable', 'bail', Rule::unique('clients')->where('company_id', $user->company()->id)];
$rules['country_id'] = 'integer|nullable|exists:countries,id';
return $rules; return $rules;
} }
@ -139,12 +128,16 @@ class StoreClientRequest extends Request
if (! array_key_exists('currency_id', $input['settings']) && isset($input['group_settings_id'])) { if (! array_key_exists('currency_id', $input['settings']) && isset($input['group_settings_id'])) {
$group_settings = GroupSetting::find($input['group_settings_id']); $group_settings = GroupSetting::find($input['group_settings_id']);
if ($group_settings && property_exists($group_settings->settings, 'currency_id') && isset($group_settings->settings->currency_id)) { if ($group_settings && property_exists($group_settings->settings, 'currency_id') && is_numeric($group_settings->settings->currency_id)) {
$input['settings']['currency_id'] = (string) $group_settings->settings->currency_id; $input['settings']['currency_id'] = (string) $group_settings->settings->currency_id;
} else { } else {
$input['settings']['currency_id'] = (string) $user->company()->settings->currency_id; $input['settings']['currency_id'] = (string) $user->company()->settings->currency_id;
} }
} elseif (! array_key_exists('currency_id', $input['settings'])) { }
elseif (! array_key_exists('currency_id', $input['settings'])) {
$input['settings']['currency_id'] = (string) $user->company()->settings->currency_id;
}
elseif (empty($input['settings']['currency_id']) ?? true) {
$input['settings']['currency_id'] = (string) $user->company()->settings->currency_id; $input['settings']['currency_id'] = (string) $user->company()->settings->currency_id;
} }
@ -160,10 +153,13 @@ class StoreClientRequest extends Request
} }
} }
// allow setting country_id by iso code
if (isset($input['country_code'])) { if (isset($input['country_code'])) {
$input['country_id'] = $this->getCountryCode($input['country_code']); $input['country_id'] = $this->getCountryCode($input['country_code']);
} }
// allow setting country_id by iso code
if (isset($input['shipping_country_code'])) { if (isset($input['shipping_country_code'])) {
$input['shipping_country_id'] = $this->getCountryCode($input['shipping_country_code']); $input['shipping_country_id'] = $this->getCountryCode($input['shipping_country_code']);
} }
@ -173,10 +169,14 @@ class StoreClientRequest extends Request
unset($input['number']); unset($input['number']);
} }
// prevent xss injection
if (array_key_exists('name', $input)) { if (array_key_exists('name', $input)) {
$input['name'] = strip_tags($input['name']); $input['name'] = strip_tags($input['name']);
} }
//If you want to validate, the prop must be set.
$input['id'] = null;
$this->replace($input); $this->replace($input);
} }

View File

@ -60,17 +60,11 @@ class UpdateClientRequest extends Request
$rules['company_logo'] = 'mimes:jpeg,jpg,png,gif|max:10000'; $rules['company_logo'] = 'mimes:jpeg,jpg,png,gif|max:10000';
$rules['industry_id'] = 'integer|nullable'; $rules['industry_id'] = 'integer|nullable';
$rules['size_id'] = 'integer|nullable'; $rules['size_id'] = 'integer|nullable';
$rules['country_id'] = 'integer|nullable'; $rules['country_id'] = 'integer|nullable|exists:countries,id';
$rules['shipping_country_id'] = 'integer|nullable'; $rules['shipping_country_id'] = 'integer|nullable|exists:countries,id';
$rules['classification'] = 'bail|sometimes|nullable|in:individual,business,company,partnership,trust,charity,government,other'; $rules['classification'] = 'bail|sometimes|nullable|in:individual,business,company,partnership,trust,charity,government,other';
$rules['id_number'] = ['sometimes', 'bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)->ignore($this->client->id)];
if ($this->id_number) { $rules['number'] = ['sometimes', 'bail', Rule::unique('clients')->where('company_id', $user->company()->id)->ignore($this->client->id)];
$rules['id_number'] = Rule::unique('clients')->where('company_id', $user->company()->id)->ignore($this->client->id);
}
if ($this->number) {
$rules['number'] = Rule::unique('clients')->where('company_id', $user->company()->id)->ignore($this->client->id);
}
$rules['settings'] = new ValidClientGroupSettingsRule(); $rules['settings'] = new ValidClientGroupSettingsRule();
$rules['contacts'] = 'array'; $rules['contacts'] = 'array';
@ -112,6 +106,9 @@ class UpdateClientRequest extends Request
if (array_key_exists('settings', $input) && ! array_key_exists('currency_id', $input['settings'])) { if (array_key_exists('settings', $input) && ! array_key_exists('currency_id', $input['settings'])) {
$input['settings']['currency_id'] = (string) $user->company()->settings->currency_id; $input['settings']['currency_id'] = (string) $user->company()->settings->currency_id;
} }
elseif (empty($input['settings']['currency_id']) ?? true) {
$input['settings']['currency_id'] = (string) $user->company()->settings->currency_id;
}
if (isset($input['language_code'])) { if (isset($input['language_code'])) {
$input['settings']['language_id'] = $this->getLanguageId($input['language_code']); $input['settings']['language_id'] = $this->getLanguageId($input['language_code']);
@ -127,9 +124,35 @@ class UpdateClientRequest extends Request
$input['name'] = strip_tags($input['name']); $input['name'] = strip_tags($input['name']);
} }
// allow setting country_id by iso code
if (isset($input['country_code'])) {
$input['country_id'] = $this->getCountryCode($input['country_code']);
}
// allow setting country_id by iso code
if (isset($input['shipping_country_code'])) {
$input['shipping_country_id'] = $this->getCountryCode($input['shipping_country_code']);
}
$this->replace($input); $this->replace($input);
} }
private function getCountryCode($country_code)
{
$countries = Cache::get('countries');
$country = $countries->filter(function ($item) use ($country_code) {
return $item->iso_3166_2 == $country_code || $item->iso_3166_3 == $country_code;
})->first();
if ($country) {
return (string) $country->id;
}
return '';
}
private function getLanguageId($language_code) private function getLanguageId($language_code)
{ {
$languages = Cache::get('languages'); $languages = Cache::get('languages');

View File

@ -57,7 +57,7 @@ class StoreCompanyRequest extends Request
} }
$rules['smtp_host'] = 'sometimes|string|nullable'; $rules['smtp_host'] = 'sometimes|string|nullable';
$rules['smtp_port'] = 'sometimes|string|nullable'; $rules['smtp_port'] = 'sometimes|integer|nullable';
$rules['smtp_encryption'] = 'sometimes|string'; $rules['smtp_encryption'] = 'sometimes|string';
$rules['smtp_local_domain'] = 'sometimes|string|nullable'; $rules['smtp_local_domain'] = 'sometimes|string|nullable';
$rules['smtp_encryption'] = 'sometimes|string|nullable'; $rules['smtp_encryption'] = 'sometimes|string|nullable';
@ -96,6 +96,10 @@ class StoreCompanyRequest extends Request
unset($input['smtp_password']); unset($input['smtp_password']);
} }
if(isset($input['smtp_port'])) {
$input['smtp_port'] = (int) $input['smtp_port'];
}
if(isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer'])) if(isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer']))
$input['smtp_verify_peer'] == 'true' ? true : false; $input['smtp_verify_peer'] == 'true' ? true : false;

View File

@ -59,7 +59,7 @@ class UpdateCompanyRequest extends Request
$rules['e_invoice_certificate'] = 'sometimes|nullable|file|mimes:p12,pfx,pem,cer,crt,der,txt,p7b,spc,bin'; $rules['e_invoice_certificate'] = 'sometimes|nullable|file|mimes:p12,pfx,pem,cer,crt,der,txt,p7b,spc,bin';
$rules['smtp_host'] = 'sometimes|string|nullable'; $rules['smtp_host'] = 'sometimes|string|nullable';
$rules['smtp_port'] = 'sometimes|string|nullable'; $rules['smtp_port'] = 'sometimes|integer|nullable';
$rules['smtp_encryption'] = 'sometimes|string|nullable'; $rules['smtp_encryption'] = 'sometimes|string|nullable';
$rules['smtp_local_domain'] = 'sometimes|string|nullable'; $rules['smtp_local_domain'] = 'sometimes|string|nullable';
// $rules['smtp_verify_peer'] = 'sometimes|string'; // $rules['smtp_verify_peer'] = 'sometimes|string';
@ -105,6 +105,10 @@ class UpdateCompanyRequest extends Request
unset($input['smtp_password']); unset($input['smtp_password']);
} }
if(isset($input['smtp_port'])) {
$input['smtp_port'] = (int)$input['smtp_port'];
}
if(isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer'])) { if(isset($input['smtp_verify_peer']) && is_string($input['smtp_verify_peer'])) {
$input['smtp_verify_peer'] == 'true' ? true : false; $input['smtp_verify_peer'] == 'true' ? true : false;
} }

View File

@ -77,6 +77,7 @@ class StoreInvoiceRequest extends Request
$rules['exchange_rate'] = 'bail|sometimes|numeric'; $rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['partial'] = 'bail|sometimes|nullable|numeric|gte:0'; $rules['partial'] = 'bail|sometimes|nullable|numeric|gte:0';
$rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date']; $rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date'];
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date'];
return $rules; return $rules;
@ -112,6 +113,12 @@ class StoreInvoiceRequest extends Request
$input['exchange_rate'] = 1; $input['exchange_rate'] = 1;
} }
//handles edge case where we need for force set the due date of the invoice.
if((isset($input['partial_due_date']) && strlen($input['partial_due_date']) > 1) && (!array_key_exists('due_date', $input) || (empty($input['due_date']) && empty($this->invoice->due_date)))) {
$client = \App\Models\Client::withTrashed()->find($input['client_id']);
$input['due_date'] = \Illuminate\Support\Carbon::parse($input['date'])->addDays($client->getSetting('payment_terms'))->format('Y-m-d');
}
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -78,6 +78,7 @@ class UpdateInvoiceRequest extends Request
$rules['exchange_rate'] = 'bail|sometimes|numeric'; $rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['partial'] = 'bail|sometimes|nullable|numeric'; $rules['partial'] = 'bail|sometimes|nullable|numeric';
$rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date', 'before:due_date']; $rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date', 'before:due_date'];
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date'];
return $rules; return $rules;
@ -107,6 +108,12 @@ class UpdateInvoiceRequest extends Request
$input['exchange_rate'] = 1; $input['exchange_rate'] = 1;
} }
//handles edge case where we need for force set the due date of the invoice.
if((isset($input['partial_due_date']) && strlen($input['partial_due_date']) > 1) && (!array_key_exists('due_date', $input) || (empty($input['due_date']) && empty($this->invoice->due_date)))) {
$client = \App\Models\Client::withTrashed()->find($input['client_id']);
$input['due_date'] = \Illuminate\Support\Carbon::parse($input['date'])->addDays($client->getSetting('payment_terms'))->format('Y-m-d');
}
$this->replace($input); $this->replace($input);
} }

View File

@ -44,6 +44,7 @@ class StoreProjectRequest extends Request
$rules['name'] = 'required'; $rules['name'] = 'required';
$rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id; $rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id;
$rules['budgeted_hours'] = 'sometimes|numeric';
if (isset($this->number)) { if (isset($this->number)) {
$rules['number'] = Rule::unique('projects')->where('company_id', $user->company()->id); $rules['number'] = Rule::unique('projects')->where('company_id', $user->company()->id);
@ -74,6 +75,9 @@ class StoreProjectRequest extends Request
$input['color'] = ''; $input['color'] = '';
} }
if(array_key_exists('budgeted_hours', $input) && empty($input['budgeted_hours']))
$input['budgeted_hours'] = 0;
$this->replace($input); $this->replace($input);
} }

View File

@ -45,6 +45,8 @@ class UpdateProjectRequest extends Request
$rules['number'] = Rule::unique('projects')->where('company_id', $user->company()->id)->ignore($this->project->id); $rules['number'] = Rule::unique('projects')->where('company_id', $user->company()->id)->ignore($this->project->id);
} }
$rules['budgeted_hours'] = 'sometimes|numeric';
if ($this->file('documents') && is_array($this->file('documents'))) { if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->file_validation; $rules['documents.*'] = $this->file_validation;
} elseif ($this->file('documents')) { } elseif ($this->file('documents')) {
@ -74,6 +76,10 @@ class UpdateProjectRequest extends Request
$input['color'] = ''; $input['color'] = '';
} }
if(array_key_exists('budgeted_hours', $input) && empty($input['budgeted_hours'])) {
$input['budgeted_hours'] = 0;
}
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -37,7 +37,8 @@ class GenericReportRequest extends Request
'start_date' => 'bail|required_if:date_range,custom|nullable|date', 'start_date' => 'bail|required_if:date_range,custom|nullable|date',
'report_keys' => 'present|array', 'report_keys' => 'present|array',
'send_email' => 'required|bool', 'send_email' => 'required|bool',
'document_email_attachment' => 'sometimes|bool' 'document_email_attachment' => 'sometimes|bool',
'include_deleted' => 'required|bool',
// 'status' => 'sometimes|string|nullable|in:all,draft,sent,viewed,paid,unpaid,overdue', // 'status' => 'sometimes|string|nullable|in:all,draft,sent,viewed,paid,unpaid,overdue',
]; ];
} }
@ -63,6 +64,8 @@ class GenericReportRequest extends Request
$input['end_date'] = null; $input['end_date'] = null;
} }
$input['include_deleted'] = array_key_exists('include_deleted', $input) ? filter_var($input['include_deleted'], FILTER_VALIDATE_BOOLEAN) : false;
$input['user_id'] = auth()->user()->id; $input['user_id'] = auth()->user()->id;
$this->replace($input); $this->replace($input);

View File

@ -36,18 +36,46 @@ class CheckSmtpRequest extends Request
public function rules() public function rules()
{ {
return [ return [
'smtp_host' => 'sometimes|nullable|string|min:3',
'smtp_port' => 'sometimes|nullable|integer',
'smtp_username' => 'sometimes|nullable|string|min:3',
'smtp_password' => 'sometimes|nullable|string|min:3',
]; ];
} }
public function prepareForValidation() public function prepareForValidation()
{ {
/** @var \App\Models\User $user */
$user = auth()->user();
$company = $user->company();
$input = $this->input(); $input = $this->input();
if(isset($input['smtp_username']) && $input['smtp_username'] == '********') if(isset($input['smtp_username']) && $input['smtp_username'] == '********'){
unset($input['smtp_username']); // unset($input['smtp_username']);
$input['smtp_username'] = $company->smtp_username;
}
if(isset($input['smtp_password'])&& $input['smtp_password'] == '********'){
// unset($input['smtp_password']);
$input['smtp_password'] = $company->smtp_password;
}
if(isset($input['smtp_host']) && strlen($input['smtp_host']) >=3){
}
else {
$input['smtp_host'] = $company->smtp_host;
}
if(isset($input['smtp_port']) && strlen($input['smtp_port']) >= 3) {
} else {
$input['smtp_port'] = $company->smtp_port;
}
if(isset($input['smtp_password'])&& $input['smtp_password'] == '********')
unset($input['smtp_password']);
$this->replace($input); $this->replace($input);
} }

View File

@ -13,11 +13,30 @@ namespace App\Http\Requests\TaskScheduler;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\Scheduler\ValidClientIds; use App\Http\ValidationRules\Scheduler\ValidClientIds;
use App\Utils\Traits\MakesHash;
class StoreSchedulerRequest extends Request class StoreSchedulerRequest extends Request
{ {
use MakesHash; public array $client_statuses = [
'all',
'draft',
'paid',
'unpaid',
'overdue',
'pending',
'invoiced',
'logged',
'partial',
'applied',
'active',
'paused',
'completed',
'approved',
'expired',
'upcoming',
'converted',
'uninvoiced',
];
/** /**
* Determine if the user is authorized to make this request. * Determine if the user is authorized to make this request.
* *
@ -47,7 +66,7 @@ class StoreSchedulerRequest extends Request
'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'], 'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'],
'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'], 'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'],
'parameters.entity_id' => ['bail', 'sometimes', 'string'], 'parameters.entity_id' => ['bail', 'sometimes', 'string'],
'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report','in:vendor,purchase_order_item,purchase_order,ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,activity,client,contact,client_contact,credit,document,expense,invoice,invoice_item,quote,quote_item,recurring_invoice,payment,product,task'], 'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report','in:vendor,purchase_order_item,purchase_order,ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,activity,activities,client,clients,client_contact,client_contacts,credit,credits,document,documents,expense,expenses,invoice,invoices,invoice_item,invoice_items,quote,quotes,quote_item,quote_items,recurring_invoice,recurring_invoices,payment,payments,product,products,task,tasks'],
'parameters.date_key' => ['bail','sometimes', 'string'], 'parameters.date_key' => ['bail','sometimes', 'string'],
'parameters.status' => ['bail','sometimes', 'nullable', 'string'], 'parameters.status' => ['bail','sometimes', 'nullable', 'string'],
]; ];
@ -73,10 +92,18 @@ class StoreSchedulerRequest extends Request
if(isset($input['parameters']['status'])) { if(isset($input['parameters']['status'])) {
$task_statuses = [];
if(isset($input['parameters']['report_name']) && $input['parameters']['report_name'] == 'task') {
$task_statuses = array_diff(explode(",", $input['parameters']['status']), $this->client_statuses);
}
$input['parameters']['status'] = collect(explode(",", $input['parameters']['status'])) $input['parameters']['status'] = collect(explode(",", $input['parameters']['status']))
->filter(function ($status) { ->filter(function ($status) {
return in_array($status, ['all','draft','paid','unpaid','overdue']); return in_array($status, $this->client_statuses);
})->implode(",") ?? ''; })->merge($task_statuses)
->implode(",") ?? '';
} }
$this->replace($input); $this->replace($input);

View File

@ -16,6 +16,27 @@ use App\Http\ValidationRules\Scheduler\ValidClientIds;
class UpdateSchedulerRequest extends Request class UpdateSchedulerRequest extends Request
{ {
public array $client_statuses = [
'all',
'draft',
'paid',
'unpaid',
'overdue',
'pending',
'invoiced',
'logged',
'partial',
'applied',
'active',
'paused',
'completed',
'approved',
'expired',
'upcoming',
'converted',
'uninvoiced',
];
/** /**
* Determine if the user is authorized to make this request. * Determine if the user is authorized to make this request.
* *
@ -45,9 +66,9 @@ class UpdateSchedulerRequest extends Request
'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'], 'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'],
'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'], 'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'],
'parameters.entity_id' => ['bail', 'sometimes', 'string'], 'parameters.entity_id' => ['bail', 'sometimes', 'string'],
'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report','in:vendor,purchase_order_item,purchase_order,ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,activity,client,contact,client_contact,credit,document,expense,invoice,invoice_item,quote,quote_item,recurring_invoice,payment,product,task'], 'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report','in:vendor,purchase_order_item,purchase_order,ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,activity,activities,client,clients,client_contact,client_contacts,credit,credits,document,documents,expense,expenses,invoice,invoices,invoice_item,invoice_items,quote,quotes,quote_item,quote_items,recurring_invoice,recurring_invoices,payment,payments,product,products,task,tasks'],
'parameters.date_key' => ['bail','sometimes', 'string'], 'parameters.date_key' => ['bail','sometimes', 'string'],
'parameters.status' => ['bail','sometimes', 'string'], 'parameters.status' => ['bail','sometimes', 'nullable', 'string'],
]; ];
return $rules; return $rules;
@ -71,10 +92,18 @@ class UpdateSchedulerRequest extends Request
if(isset($input['parameters']['status'])) { if(isset($input['parameters']['status'])) {
$task_statuses = [];
if(isset($input['parameters']['report_name']) && $input['parameters']['report_name'] == 'task') {
$task_statuses = array_diff(explode(",", $input['parameters']['status']), $this->client_statuses);
}
$input['parameters']['status'] = collect(explode(",", $input['parameters']['status'])) $input['parameters']['status'] = collect(explode(",", $input['parameters']['status']))
->filter(function ($status) { ->filter(function ($status) {
return in_array($status, ['all','draft','paid','unpaid','overdue']); return in_array($status, $this->client_statuses);
})->implode(",") ?? ''; })->merge($task_statuses)
->implode(",") ?? '';
} }
$this->replace($input); $this->replace($input);

View File

@ -95,6 +95,8 @@ class StoreUserRequest extends Request
$input['last_name'] = strip_tags($input['last_name']); $input['last_name'] = strip_tags($input['last_name']);
} }
$input['id'] = null;
$this->replace($input); $this->replace($input);
} }

View File

@ -23,7 +23,10 @@ class StoreWebhookRequest extends Request
*/ */
public function authorize(): bool public function authorize(): bool
{ {
return auth()->user()->isAdmin() && auth()->user()->account->hasFeature(Account::FEATURE_API); /** @var \App\Models\User $user */
$user = auth()->user();
return $user->isAdmin() && $user->account->hasFeature(Account::FEATURE_API);
} }
public function rules() public function rules()
@ -31,7 +34,6 @@ class StoreWebhookRequest extends Request
return [ return [
'target_url' => 'bail|required|url', 'target_url' => 'bail|required|url',
'event_id' => 'bail|required', 'event_id' => 'bail|required',
// 'headers' => 'bail|sometimes|json',
'rest_method' => 'required|in:post,put' 'rest_method' => 'required|in:post,put'
]; ];
} }
@ -43,8 +45,6 @@ class StoreWebhookRequest extends Request
if (!isset($input['rest_method'])) { if (!isset($input['rest_method'])) {
$input['rest_method'] = 'post'; $input['rest_method'] = 'post';
} }
// if(isset($input['headers']) && count($input['headers']) == 0)
// $input['headers'] = null;
$this->replace($input); $this->replace($input);
} }

View File

@ -153,6 +153,7 @@ class BaseImport
} }
/** @phpstan-ignore-next-line **/
return $bestDelimiter ?? ','; return $bestDelimiter ?? ',';
} }

View File

@ -98,19 +98,19 @@ class InvoiceTransformer extends BaseTransformer
$invoice_data, $invoice_data,
'invoice.partial_due_date' 'invoice.partial_due_date'
), ),
'custom_surcharge1' => $this->getString( 'custom_surcharge1' => $this->getFloat(
$invoice_data, $invoice_data,
'invoice.custom_surcharge1' 'invoice.custom_surcharge1'
), ),
'custom_surcharge2' => $this->getString( 'custom_surcharge2' => $this->getFloat(
$invoice_data, $invoice_data,
'invoice.custom_surcharge2' 'invoice.custom_surcharge2'
), ),
'custom_surcharge3' => $this->getString( 'custom_surcharge3' => $this->getFloat(
$invoice_data, $invoice_data,
'invoice.custom_surcharge3' 'invoice.custom_surcharge3'
), ),
'custom_surcharge4' => $this->getString( 'custom_surcharge4' => $this->getFloat(
$invoice_data, $invoice_data,
'invoice.custom_surcharge4' 'invoice.custom_surcharge4'
), ),

View File

@ -114,23 +114,26 @@ class ProcessBankTransactionsNordigen implements ShouldQueue
private function updateAccount() private function updateAccount()
{ {
if (!$this->nordigen->isAccountActive($this->bank_integration->nordigen_account_id)) { $is_account_active = $this->nordigen->isAccountActive($this->bank_integration->nordigen_account_id);
$account = $this->nordigen->getAccount($this->bank_integration->nordigen_account_id);
if (!$is_account_active || !$account) {
$this->bank_integration->disabled_upstream = true; $this->bank_integration->disabled_upstream = true;
$this->bank_integration->save(); $this->bank_integration->save();
$this->stop_loop = false; $this->stop_loop = false;
nlog("Nordigen: account inactive: " . $this->bank_integration->nordigen_account_id); nlog("Nordigen: account inactive: " . $this->bank_integration->nordigen_account_id);
// @turbo124 @todo send email for expired account
$this->nordigen->disabledAccountEmail($this->bank_integration); $this->nordigen->disabledAccountEmail($this->bank_integration);
return; return;
} }
$this->nordigen_account = $this->nordigen->getAccount($this->bank_integration->nordigen_account_id); $this->nordigen_account = $account;
$this->bank_integration->disabled_upstream = false; $this->bank_integration->disabled_upstream = false;
$this->bank_integration->bank_account_status = $this->nordigen_account['account_status']; $this->bank_integration->bank_account_status = $account['account_status'];
$this->bank_integration->balance = $this->nordigen_account['current_balance']; $this->bank_integration->balance = $account['current_balance'];
$this->bank_integration->save(); $this->bank_integration->save();
} }
@ -138,7 +141,7 @@ class ProcessBankTransactionsNordigen implements ShouldQueue
private function processTransactions() private function processTransactions()
{ {
//Get transaction count object //Get transaction count object
$transactions = $this->nordigen->getTransactions($this->bank_integration->nordigen_account_id, $this->from_date); $transactions = $this->nordigen->getTransactions($this->company, $this->bank_integration->nordigen_account_id, $this->from_date);
//if no transactions, update the from_date and move on //if no transactions, update the from_date and move on
if (count($transactions) == 0) { if (count($transactions) == 0) {

View File

@ -69,10 +69,9 @@ class CompanyExport implements ShouldQueue
{ {
MultiDB::setDb($this->company->db); MultiDB::setDb($this->company->db);
$this->file_name = date('Y-m-d') . '_' . str_replace([" ", "/"], ["_",""], $this->company->present()->name() . '_' . $this->company->company_key . '.json'); $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); $this->writer = new File(sys_get_temp_dir().'/'.$this->file_name);
set_time_limit(0); set_time_limit(0);
@ -114,8 +113,6 @@ class CompanyExport implements ShouldQueue
return $user; return $user;
})->all(); })->all();
$x = $this->writer->collection('users'); $x = $this->writer->collection('users');
$x->addItems($this->export_data['users']); $x->addItems($this->export_data['users']);
$this->export_data = null; $this->export_data = null;
@ -667,7 +664,7 @@ class CompanyExport implements ShouldQueue
private function zipAndSend() private function zipAndSend()
{ {
$zip_path = \Illuminate\Support\Str::ascii(str_replace(".json", ".zip", $this->file_name)); $zip_path = sys_get_temp_dir().'/'.\Illuminate\Support\Str::ascii(str_replace(".json", ".zip", $this->file_name));
$zip = new \ZipArchive(); $zip = new \ZipArchive();
@ -675,8 +672,8 @@ class CompanyExport implements ShouldQueue
nlog("cannot open {$zip_path}"); nlog("cannot open {$zip_path}");
} }
$zip->addFile($this->file_name); $zip->addFile(sys_get_temp_dir().'/'.$this->file_name, 'backup.json');
$zip->renameName($this->file_name, 'backup.json'); // $zip->renameName($this->file_name, 'backup.json');
$zip->close(); $zip->close();
@ -686,8 +683,8 @@ class CompanyExport implements ShouldQueue
unlink($zip_path); unlink($zip_path);
} }
if(file_exists($this->file_name)) { if(file_exists(sys_get_temp_dir().'/'.$this->file_name)) {
unlink($this->file_name); unlink(sys_get_temp_dir().'/'.$this->file_name);
} }
if(Ninja::isSelfHost()) { if(Ninja::isSelfHost()) {
@ -717,8 +714,8 @@ class CompanyExport implements ShouldQueue
if (Ninja::isHosted()) { if (Ninja::isHosted()) {
sleep(3); sleep(3);
if(file_exists($zip_path)) { if(file_exists(sys_get_temp_dir().'/'.$zip_path)) {
unlink($zip_path); unlink(sys_get_temp_dir().'/'.$zip_path);
} }
} }
} }

View File

@ -311,8 +311,11 @@ class CompanyImport implements ShouldQueue
} }
} }
unlink($tmp_file); if(file_exists($tmp_file))
unlink(Storage::path($this->file_location)); unlink($tmp_file);
if(Storage::exists($this->file_location))
unlink(Storage::path($this->file_location));
} }
// //

View File

@ -150,7 +150,8 @@ class NinjaMailerJob implements ShouldQueue
->send($mailable); ->send($mailable);
/* Count the amount of emails sent across all the users accounts */ /* Count the amount of emails sent across all the users accounts */
Cache::increment("email_quota" . $this->company->account->key);
$this->incrementEmailCounter();
LightLogs::create(new EmailSuccess($this->nmo->company->company_key, $this->nmo->mailable->subject)) LightLogs::create(new EmailSuccess($this->nmo->company->company_key, $this->nmo->mailable->subject))
->send(); ->send();
@ -226,6 +227,12 @@ class NinjaMailerJob implements ShouldQueue
$this->cleanUpMailers(); $this->cleanUpMailers();
} }
private function incrementEmailCounter(): void
{
if(in_array($this->mailer, ['default','mailgun']))
Cache::increment("email_quota".$this->company->account->key);
}
/** /**
* Entity notification when an email fails to send * Entity notification when an email fails to send
* *
@ -284,13 +291,21 @@ class NinjaMailerJob implements ShouldQueue
$this->mailer = 'postmark'; $this->mailer = 'postmark';
$this->client_postmark_secret = config('services.postmark-outlook.token'); $this->client_postmark_secret = config('services.postmark-outlook.token');
if (property_exists($this->nmo->settings, 'email_from_name') && strlen($this->nmo->settings->email_from_name) > 1) {
$email_from_name = $this->nmo->settings->email_from_name;
} else {
$email_from_name = $this->company->present()->name();
}
$this->nmo $this->nmo
->mailable ->mailable
->from('maildelivery@invoice.services', 'Invoice Ninja'); ->from(config('services.postmark-outlook.from.address'), $email_from_name);
return $this; return $this;
} }
} catch(\Exception $e) { } catch(\Exception $e) {
nlog("problem switching outlook driver - hosted");
nlog($e->getMessage()); nlog($e->getMessage());
} }
} }
@ -321,11 +336,10 @@ class NinjaMailerJob implements ShouldQueue
$this->mailer = 'mailgun'; $this->mailer = 'mailgun';
$this->setMailgunMailer(); $this->setMailgunMailer();
return $this; return $this;
case 'client_brevo': case 'smtp':
$this->mailer = 'brevo'; $this->mailer = 'smtp';
$this->setBrevoMailer(); $this->configureSmtpMailer();
return $this; return $this;
default: default:
break; break;
} }
@ -337,6 +351,48 @@ class NinjaMailerJob implements ShouldQueue
return $this; return $this;
} }
private function configureSmtpMailer(): void
{
$company = $this->company;
$smtp_host = $company->smtp_host;
$smtp_port = $company->smtp_port;
$smtp_username = $company->smtp_username;
$smtp_password = $company->smtp_password;
$smtp_encryption = $company->smtp_encryption ?? 'tls';
$smtp_local_domain = strlen($company->smtp_local_domain) > 2 ? $company->smtp_local_domain : null;
$smtp_verify_peer = $company->smtp_verify_peer ?? true;
config([
'mail.mailers.smtp' => [
'transport' => 'smtp',
'host' => $smtp_host,
'port' => $smtp_port,
'username' => $smtp_username,
'password' => $smtp_password,
'encryption' => $smtp_encryption,
'local_domain' => $smtp_local_domain,
'verify_peer' => $smtp_verify_peer,
'timeout' => 30,
],
]);
if (property_exists($this->nmo->settings, 'email_from_name') && strlen($this->nmo->settings->email_from_name) > 1) {
$email_from_name = $this->nmo->settings->email_from_name;
} else {
$email_from_name = $this->company->present()->name();
}
$user = $this->resolveSendingUser();
$sending_email = (isset($this->nmo->settings->custom_sending_email) && stripos($this->nmo->settings->custom_sending_email, "@")) ? $this->nmo->settings->custom_sending_email : $user->email;
$this->nmo
->mailable
->from($sending_email, $email_from_name);
}
/** /**
* Allows configuration of multiple mailers * Allows configuration of multiple mailers
* per company for use by self hosted users * per company for use by self hosted users

View File

@ -60,6 +60,7 @@ class TaskScheduler implements ShouldQueue
nlog("Doing job {$scheduler->name}"); nlog("Doing job {$scheduler->name}");
try { try {
//@var \App\Models\Schedule $scheduler
$scheduler->service()->runTask(); $scheduler->service()->runTask();
} catch(\Exception $e) { } catch(\Exception $e) {
nlog($e->getMessage()); nlog($e->getMessage());

View File

@ -420,7 +420,7 @@ class Import implements ShouldQueue
if (Ninja::isHosted()) { if (Ninja::isHosted()) {
$data['subdomain'] = str_replace("_", "", $data['subdomain']); $data['subdomain'] = str_replace("_", "", ($data['subdomain'] ?? ''));
if (!MultiDB::checkDomainAvailable($data['subdomain'])) { if (!MultiDB::checkDomainAvailable($data['subdomain'])) {
$data['subdomain'] = MultiDB::randomSubdomainGenerator(); $data['subdomain'] = MultiDB::randomSubdomainGenerator();

View File

@ -26,7 +26,6 @@ use App\Utils\Traits\MakesReminders;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Spatie\OpenTelemetry\Jobs\TraceAware;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;

View File

@ -123,7 +123,7 @@ class WebhookSingle implements ShouldQueue
]); ]);
(new SystemLogger( (new SystemLogger(
array_merge((array) $response, $data), ['message' => $response->getHeaders(), 'body' => $data],
SystemLog::CATEGORY_WEBHOOK, SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_SUCCESS, SystemLog::EVENT_WEBHOOK_SUCCESS,
SystemLog::TYPE_WEBHOOK_RESPONSE, SystemLog::TYPE_WEBHOOK_RESPONSE,
@ -136,7 +136,7 @@ class WebhookSingle implements ShouldQueue
nlog($e->getMessage()); nlog($e->getMessage());
(new SystemLogger( (new SystemLogger(
['message' => "Error connecting to ". $subscription->target_url], ['message' => "Error connecting to ". $subscription->target_url, 'body' => $data],
SystemLog::CATEGORY_WEBHOOK, SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE, SystemLog::EVENT_WEBHOOK_FAILURE,
SystemLog::TYPE_WEBHOOK_RESPONSE, SystemLog::TYPE_WEBHOOK_RESPONSE,
@ -152,7 +152,7 @@ class WebhookSingle implements ShouldQueue
$message = "There was a problem when connecting to {$subscription->target_url} => status code ". $e->getResponse()->getStatusCode(). " This webhook call will be suspended until further action is taken."; $message = "There was a problem when connecting to {$subscription->target_url} => status code ". $e->getResponse()->getStatusCode(). " This webhook call will be suspended until further action is taken.";
(new SystemLogger( (new SystemLogger(
['message' => $message], ['message' => $message, 'body' => $data],
SystemLog::CATEGORY_WEBHOOK, SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE, SystemLog::EVENT_WEBHOOK_FAILURE,
SystemLog::TYPE_WEBHOOK_RESPONSE, SystemLog::TYPE_WEBHOOK_RESPONSE,
@ -170,7 +170,7 @@ class WebhookSingle implements ShouldQueue
nlog($message); nlog($message);
(new SystemLogger( (new SystemLogger(
['message' => $message], ['message' => $message, 'body' => $data],
SystemLog::CATEGORY_WEBHOOK, SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE, SystemLog::EVENT_WEBHOOK_FAILURE,
SystemLog::TYPE_WEBHOOK_RESPONSE, SystemLog::TYPE_WEBHOOK_RESPONSE,
@ -192,7 +192,7 @@ class WebhookSingle implements ShouldQueue
$message = "There was a problem when connecting to {$subscription->target_url} => status code ". $e->getResponse()->getStatusCode(). " no retry attempted."; $message = "There was a problem when connecting to {$subscription->target_url} => status code ". $e->getResponse()->getStatusCode(). " no retry attempted.";
(new SystemLogger( (new SystemLogger(
['message' => $message], ['message' => $message, 'body' => $data],
SystemLog::CATEGORY_WEBHOOK, SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE, SystemLog::EVENT_WEBHOOK_FAILURE,
SystemLog::TYPE_WEBHOOK_RESPONSE, SystemLog::TYPE_WEBHOOK_RESPONSE,
@ -208,7 +208,7 @@ class WebhookSingle implements ShouldQueue
$error = json_decode($e->getResponse()->getBody()->getContents()); $error = json_decode($e->getResponse()->getBody()->getContents());
(new SystemLogger( (new SystemLogger(
['message' => $error], ['message' => $error, 'body' => $data],
SystemLog::CATEGORY_WEBHOOK, SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE, SystemLog::EVENT_WEBHOOK_FAILURE,
SystemLog::TYPE_WEBHOOK_RESPONSE, SystemLog::TYPE_WEBHOOK_RESPONSE,
@ -220,7 +220,7 @@ class WebhookSingle implements ShouldQueue
$error = json_decode($e->getResponse()->getBody()->getContents()); $error = json_decode($e->getResponse()->getBody()->getContents());
(new SystemLogger( (new SystemLogger(
['message' => $error], ['message' => $error, 'body' => $data],
SystemLog::CATEGORY_WEBHOOK, SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE, SystemLog::EVENT_WEBHOOK_FAILURE,
SystemLog::TYPE_WEBHOOK_RESPONSE, SystemLog::TYPE_WEBHOOK_RESPONSE,
@ -232,7 +232,7 @@ class WebhookSingle implements ShouldQueue
nlog($e->getCode()); nlog($e->getCode());
(new SystemLogger( (new SystemLogger(
$e->getMessage(), ['message' => $e->getMessage(), 'body' => $data],
SystemLog::CATEGORY_WEBHOOK, SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE, SystemLog::EVENT_WEBHOOK_FAILURE,
SystemLog::TYPE_WEBHOOK_RESPONSE, SystemLog::TYPE_WEBHOOK_RESPONSE,

View File

@ -149,6 +149,7 @@ class PdfSlot extends Component
return render('components.livewire.pdf-slot', [ return render('components.livewire.pdf-slot', [
'invitation' => $this->invitation, 'invitation' => $this->invitation,
'entity' => $this->entity, 'entity' => $this->entity,
'settings' => $this->settings,
'data' => $this->invitation->company->settings, 'data' => $this->invitation->company->settings,
'entity_type' => $this->entity_type, 'entity_type' => $this->entity_type,
'products' => $this->getProducts(), 'products' => $this->getProducts(),

View File

@ -12,15 +12,17 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\Client;
use App\Models\Invoice;
use Livewire\Component;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use Illuminate\Support\Str;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\Invoice;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Livewire\Attributes\Computed;
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 Livewire\Component;
class RequiredClientInfo extends Component class RequiredClientInfo extends Component
{ {
@ -31,10 +33,7 @@ class RequiredClientInfo extends Component
*/ */
public $show_terms = false; public $show_terms = false;
/** public $invoice_terms;
* @var array
*/
public $invoice;
/** /**
* @var bool * @var bool
@ -49,18 +48,40 @@ class RequiredClientInfo extends Component
/** /**
* @var ClientContact * @var ClientContact
*/ */
public $contact; public $contact_id;
/** /**
* @var \App\Models\Client * @var \App\Models\Client
*/ */
public $client; public $client_id;
/** /**
* @var array * @var array
*/ */
public $countries; public $countries;
public $client_name;
public $contact_first_name;
public $contact_last_name;
public $contact_email;
public $client_phone;
public $client_address_line_1;
public $client_city;
public $client_state;
public $client_country_id;
public $client_postal_code;
public $client_shipping_address_line_1;
public $client_shipping_city;
public $client_shipping_state;
public $client_shipping_postal_code;
public $client_shipping_country_id;
public $client_custom_value1;
public $client_custom_value2;
public $client_custom_value3;
public $client_custom_value4;
/** /**
* Mappings for updating the database. Left side is mapping from gateway, * Mappings for updating the database. Left side is mapping from gateway,
* right side is column in database. * right side is column in database.
@ -113,50 +134,96 @@ class RequiredClientInfo extends Component
]; ];
protected $rules = [ protected $rules = [
'client.address1' => '', // 'client.address1' => '',
'client.address2' => '', // 'client.address2' => '',
'client.city' => '', // 'client.city' => '',
'client.state' => '', // 'client.state' => '',
'client.postal_code' => '', // 'client.postal_code' => '',
'client.country_id' => '', // 'client.country_id' => '',
'client.shipping_address1' => '', // 'client.shipping_address1' => '',
'client.shipping_address2' => '', // 'client.shipping_address2' => '',
'client.shipping_city' => '', // 'client.shipping_city' => '',
'client.shipping_state' => '', // 'client.shipping_state' => '',
'client.shipping_postal_code' => '', // 'client.shipping_postal_code' => '',
'client.shipping_country_id' => '', // 'client.shipping_country_id' => '',
'contact.first_name' => '', // 'contact.first_name' => '',
'contact.last_name' => '', // 'contact.last_name' => '',
'contact.email' => '', // 'contact.email' => '',
'client.name' => '', // 'client.name' => '',
'client.website' => '', // 'client.website' => '',
'client.phone' => '', // 'client.phone' => '',
'client.custom_value1' => '', // 'client.custom_value1' => '',
'client.custom_value2' => '', // 'client.custom_value2' => '',
'client.custom_value3' => '', // 'client.custom_value3' => '',
'client.custom_value4' => '', // 'client.custom_value4' => '',
'client_name' => '',
'client_website' => '',
'client_phone' => '',
'client_address_line_1' => '',
'client_address_line_2' => '',
'client_city' => '',
'client_state' => '',
'client_postal_code' => '',
'client_country_id' => '',
'client_shipping_address_line_1' => '',
'client_shipping_address_line_2' => '',
'client_shipping_city' => '',
'client_shipping_state' => '',
'client_shipping_postal_code' => '',
'client_shipping_country_id' => '',
'client_custom_value1' => '',
'client_custom_value2' => '',
'client_custom_value3' => '',
'client_custom_value4' => '',
'contact_first_name' => '',
'contact_last_name' => '',
'contact_email' => '',
]; ];
public $show_form = false; public $show_form = false;
public $company; public $company_id;
public $company_gateway_id; public $company_gateway_id;
public $db;
public function mount() public function mount()
{ {
MultiDB::setDb($this->company->db); MultiDB::setDb($this->db);
$contact = ClientContact::withTrashed()->find($this->contact_id);
$company = $contact->company;
$this->client = $this->contact->client; $this->client_name = $contact->client->name;
$this->contact_first_name = $contact->first_name;
$this->contact_last_name = $contact->last_name;
$this->contact_email = $contact->email;
$this->client_phone = $contact->client->phone;
$this->client_address_line_1 = $contact->client->address1;
$this->client_city = $contact->client->city ;
$this->client_state = $contact->client->state;
$this->client_country_id = $contact->client->country_id;
$this->client_postal_code = $contact->client->postal_code;
$this->client_shipping_address_line_1 = $contact->client->shipping_address1;
$this->client_shipping_city = $contact->client->shipping_city;
$this->client_shipping_state = $contact->client->shipping_state;
$this->client_shipping_postal_code = $contact->client->shipping_postal_code;
$this->client_shipping_country_id = $contact->client->shipping_country_id;
$this->client_custom_value1 = $contact->client->custom_value1;
$this->client_custom_value2 = $contact->client->custom_value2;
$this->client_custom_value3 = $contact->client->custom_value3;
$this->client_custom_value4 = $contact->client->custom_value4;
if ($this->company->settings->show_accept_invoice_terms && request()->query('hash')) { // $this->client = $this->contact->client;
if ($company->settings->show_accept_invoice_terms && request()->query('hash')) {
$this->show_terms = true; $this->show_terms = true;
$this->terms_accepted = false; $this->terms_accepted = false;
$this->show_form = true; $this->show_form = true;
$hash = Cache::get(request()->input('hash')); $hash = Cache::get(request()->input('hash'));
$this->invoice = Invoice::find($this->decodePrimaryKey($hash['invoice_id'])); $this->invoice_terms = Invoice::find($this->decodePrimaryKey($hash['invoice_id']))->terms;
} }
count($this->fields) > 0 || $this->show_terms count($this->fields) > 0 || $this->show_terms
@ -164,6 +231,24 @@ class RequiredClientInfo extends Component
: $this->show_form = false; : $this->show_form = false;
} }
#[Computed]
public function contact()
{
MultiDB::setDb($this->db);
return ClientContact::withTrashed()->find($this->contact_id);
}
#[Computed]
public function client()
{
MultiDB::setDb($this->db);
return ClientContact::withTrashed()->find($this->contact_id)->client;
}
public function toggleTermsAccepted() public function toggleTermsAccepted()
{ {
$this->terms_accepted = !$this->terms_accepted; $this->terms_accepted = !$this->terms_accepted;
@ -171,6 +256,10 @@ class RequiredClientInfo extends Component
public function handleSubmit(array $data): bool public function handleSubmit(array $data): bool
{ {
MultiDB::setDb($this->db);
$contact = ClientContact::withTrashed()->find($this->contact_id);
$rules = []; $rules = [];
collect($this->fields)->map(function ($field) use (&$rules) { collect($this->fields)->map(function ($field) use (&$rules) {
@ -192,7 +281,7 @@ class RequiredClientInfo extends Component
if ($this->updateClientDetails($data)) { if ($this->updateClientDetails($data)) {
$this->dispatch( $this->dispatch(
'passed-required-fields-check', 'passed-required-fields-check',
client_postal_code: $this->contact->client->postal_code client_postal_code: $contact->client->postal_code
); );
//if stripe is enabled, we want to update the customer at this point. //if stripe is enabled, we want to update the customer at this point.
@ -209,6 +298,11 @@ class RequiredClientInfo extends Component
$client = []; $client = [];
$contact = []; $contact = [];
MultiDB::setDb($this->db);
$_contact = ClientContact::withTrashed()->find($this->contact_id);
foreach ($data as $field => $value) { foreach ($data as $field => $value) {
if (Str::startsWith($field, 'client_')) { if (Str::startsWith($field, 'client_')) {
$client[$this->mappings[$field]] = $value; $client[$this->mappings[$field]] = $value;
@ -219,20 +313,43 @@ class RequiredClientInfo extends Component
} }
} }
$contact_update = $this->contact
$_contact->first_name = $this->contact_first_name;
$_contact->last_name = $this->contact_last_name;
$_contact->client->name = $this->client_name;
$_contact->email = $this->contact_email;
$_contact->client->phone = $this->client_phone;
$_contact->client->address1 = $this->client_address_line_1;
$_contact->client->city = $this->client_city;
$_contact->client->state = $this->client_state;
$_contact->client->country_id = $this->client_country_id;
$_contact->client->postal_code = $this->client_postal_code;
$_contact->client->shipping_address1 = $this->client_shipping_address_line_1;
$_contact->client->shipping_city = $this->client_shipping_city;
$_contact->client->shipping_state = $this->client_shipping_state;
$_contact->client->shipping_postal_code = $this->client_shipping_postal_code;
$_contact->client->shipping_country_id = $this->client_shipping_country_id;
$_contact->client->custom_value1 = $this->client_custom_value1;
$_contact->client->custom_value2 = $this->client_custom_value2;
$_contact->client->custom_value3 = $this->client_custom_value3;
$_contact->client->custom_value4 = $this->client_custom_value4;
$_contact->push();
$contact_update = $_contact
->fill($contact) ->fill($contact)
->push(); ->push();
$client_update = $this->contact->client $client_update = $_contact->client
->fill($client) ->fill($client)
->push(); ->push();
if ($contact_update && $client_update) { if ($_contact) {
/** @var \App\Models\CompanyGateway $cg */ /** @var \App\Models\CompanyGateway $cg */
$cg = CompanyGateway::find($this->company_gateway_id); $cg = CompanyGateway::find($this->company_gateway_id);
if ($cg && $cg->update_details) { if ($cg && $cg->update_details) {
$payment_gateway = $cg->driver($this->client)->init(); $payment_gateway = $cg->driver($_contact->client)->init();
if (method_exists($payment_gateway, "updateCustomer")) { if (method_exists($payment_gateway, "updateCustomer")) {
$payment_gateway->updateCustomer(); $payment_gateway->updateCustomer();
@ -247,11 +364,15 @@ class RequiredClientInfo extends Component
public function checkFields() public function checkFields()
{ {
MultiDB::setDb($this->db);
$_contact = ClientContact::withTrashed()->find($this->contact_id);
foreach ($this->fields as $index => $field) { foreach ($this->fields as $index => $field) {
$_field = $this->mappings[$field['name']]; $_field = $this->mappings[$field['name']];
if (Str::startsWith($field['name'], 'client_')) { if (Str::startsWith($field['name'], 'client_')) {
if (empty($this->contact->client->{$_field}) || is_null($this->contact->client->{$_field}) || in_array($_field, $this->client_address_array)) { if (empty($_contact->client->{$_field}) || is_null($_contact->client->{$_field}) || in_array($_field, $this->client_address_array)) {
$this->show_form = true; $this->show_form = true;
} else { } else {
$this->fields[$index]['filled'] = true; $this->fields[$index]['filled'] = true;
@ -259,7 +380,7 @@ class RequiredClientInfo extends Component
} }
if (Str::startsWith($field['name'], 'contact_')) { if (Str::startsWith($field['name'], 'contact_')) {
if (empty($this->contact->{$_field}) || is_null($this->contact->{$_field}) || str_contains($this->contact->{$_field}, '@example.com')) { if (empty($_contact->{$_field}) || is_null($_contact->{$_field}) || str_contains($_contact->{$_field}, '@example.com')) {
$this->show_form = true; $this->show_form = true;
} else { } else {
$this->fields[$index]['filled'] = true; $this->fields[$index]['filled'] = true;
@ -289,14 +410,18 @@ class RequiredClientInfo extends Component
public function handleCopyBilling(): void public function handleCopyBilling(): void
{ {
MultiDB::setDb($this->db);
$_contact = ClientContact::withTrashed()->find($this->contact_id);
$this->dispatch( $this->dispatch(
'update-shipping-data', 'update-shipping-data',
client_shipping_address_line_1: $this->contact->client->address1, client_shipping_address_line_1: $_contact->client->address1,
client_shipping_address_line_2: $this->contact->client->address2, client_shipping_address_line_2: $_contact->client->address2,
client_shipping_city: $this->contact->client->city, client_shipping_city: $_contact->client->city,
client_shipping_state: $this->contact->client->state, client_shipping_state: $_contact->client->state,
client_shipping_postal_code: $this->contact->client->postal_code, client_shipping_postal_code: $_contact->client->postal_code,
client_shipping_country_id: $this->contact->client->country_id, client_shipping_country_id: $_contact->client->country_id,
); );
} }

View File

@ -52,7 +52,7 @@ class ClientStatement extends Mailable
public function content() public function content()
{ {
return new Content( return new Content(
view: $this->data['company']->account->isPremium() ? 'email.template.client_premium' : 'email.template.client', view: 'email.template.client',
text: 'email.template.text', text: 'email.template.text',
with: [ with: [
'text_body' => $this->data['body'], 'text_body' => $this->data['body'],

View File

@ -75,7 +75,8 @@ class TemplateEmail extends Mailable
$template_name = 'email.template.'.$this->build_email->getTemplate(); $template_name = 'email.template.'.$this->build_email->getTemplate();
if ($this->build_email->getTemplate() == 'light' || $this->build_email->getTemplate() == 'dark') { if ($this->build_email->getTemplate() == 'light' || $this->build_email->getTemplate() == 'dark') {
$template_name = $this->company->account->isPremium() ? 'email.template.client_premium' : 'email.template.client'; // $template_name = $this->company->account->isPremium() ? 'email.template.client_premium' : 'email.template.client';
$template_name = 'email.template.client';
} }
if ($this->build_email->getTemplate() == 'custom') { if ($this->build_email->getTemplate() == 'custom') {

View File

@ -72,7 +72,8 @@ class VendorTemplateEmail extends Mailable
$template_name = 'email.template.'.$this->build_email->getTemplate(); $template_name = 'email.template.'.$this->build_email->getTemplate();
if ($this->build_email->getTemplate() == 'light' || $this->build_email->getTemplate() == 'dark') { if ($this->build_email->getTemplate() == 'light' || $this->build_email->getTemplate() == 'dark') {
$template_name = $this->company->account->isPremium() ? 'email.template.client_premium' : 'email.template.client'; // $template_name = $this->company->account->isPremium() ? 'email.template.client_premium' : 'email.template.client';
$template_name = 'email.template.client';
} }
if ($this->build_email->getTemplate() == 'custom') { if ($this->build_email->getTemplate() == 'custom') {

View File

@ -208,6 +208,29 @@ class Document extends BaseModel
return ctrans('texts.document'); return ctrans('texts.document');
} }
public function link()
{
$entity_id = $this->encodePrimaryKey($this->documentable_id);
$link = '';
match($this->documentable_type) {
'App\Models\Vendor' => $link = "/vendors/{$entity_id}",
'App\Models\Project' => $link = "/projects/{$entity_id}",
'invoices' => $link = "/invoices/{$entity_id}/edit",
'App\Models\Quote' => $link = "/quotes/{$entity_id}/edit",
'App\Models\Credit' => $link = "/credits/{$entity_id}/edit",
'App\Models\Expense' => $link = "/expenses/{$entity_id}/edit",
'App\Models\Payment' => $link = "/payments/{$entity_id}/edit",
'App\Models\Task' => $link = "/tasks/{$entity_id}/edit",
'App\Models\Client' => $link = "/clients/{$entity_id}",
'App\Models\RecurringExpense' => $link = "/recurring_expenses/{$entity_id}/edit",
'App\Models\RecurringInvoice' => $link = "/recurring_invoices/{$entity_id}/edit",
default => $link = '',
};
return $link;
}
public function compress(): mixed public function compress(): mixed
{ {

View File

@ -137,22 +137,22 @@ class Gateway extends StaticModel
case 56: case 56:
return [ return [
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded', 'payment_intent.payment_failed']],
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'customer.source.updated','payment_intent.processing', 'payment_intent.payment_failed']], GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'customer.source.updated','payment_intent.processing', 'payment_intent.payment_failed', 'charge.failed']],
GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing','payment_intent.succeeded','payment_intent.partially_funded', 'payment_intent.payment_failed']], GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing','payment_intent.succeeded','payment_intent.partially_funded', 'payment_intent.payment_failed']],
GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false], GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false],
GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false], GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false],
GatewayType::BACS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.processing', 'payment_intent.succeeded', 'mandate.updated', 'payment_intent.payment_failed']], GatewayType::BACS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.processing', 'payment_intent.succeeded', 'mandate.updated', 'payment_intent.payment_failed']],
GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', ]],
]; ];
case 39: case 39:
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']]]; //Checkout return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']]]; //Checkout

View File

@ -355,10 +355,12 @@ class RecurringInvoice extends BaseModel
public function calculateStatus(bool $new_model = false) //15-02-2024 - $new_model needed public function calculateStatus(bool $new_model = false) //15-02-2024 - $new_model needed
{ {
if($this->remaining_cycles == 0) { if($this->remaining_cycles == 0)
return self::STATUS_COMPLETED; return self::STATUS_COMPLETED;
} elseif ($new_model && $this->status_id == self::STATUS_ACTIVE && Carbon::parse($this->next_send_date)->isFuture()) elseif ($new_model && $this->status_id == self::STATUS_ACTIVE && Carbon::parse($this->next_send_date)->isFuture())
return self::STATUS_PENDING; return self::STATUS_PENDING;
elseif($this->remaining_cycles != 0 && ($this->status_id == self::STATUS_COMPLETED))
return self::STATUS_ACTIVE;
return $this->status_id; return $this->status_id;

View File

@ -180,6 +180,7 @@ class Webhook extends BaseModel
self::EVENT_DELETE_PURCHASE_ORDER, self::EVENT_DELETE_PURCHASE_ORDER,
self::EVENT_RESTORE_PURCHASE_ORDER, self::EVENT_RESTORE_PURCHASE_ORDER,
self::EVENT_ARCHIVE_PURCHASE_ORDER, self::EVENT_ARCHIVE_PURCHASE_ORDER,
self::EVENT_CREATE_PRODUCT,
self::EVENT_UPDATE_PRODUCT, self::EVENT_UPDATE_PRODUCT,
self::EVENT_DELETE_PRODUCT, self::EVENT_DELETE_PRODUCT,
self::EVENT_RESTORE_PRODUCT, self::EVENT_RESTORE_PRODUCT,

View File

@ -271,7 +271,7 @@ class CreditCard implements MethodInterface
$errors = $api_response->getErrors(); $errors = $api_response->getErrors();
} }
if (property_exists($customers, 'customers')) { if ($customers && property_exists($customers, 'customers')) {
return $customers->customers[0]->id; return $customers->customers[0]->id;
} }

View File

@ -89,6 +89,10 @@ class PaymentIntentWebhook implements ShouldQueue
} }
$company_gateway = CompanyGateway::query()->find($this->company_gateway_id); $company_gateway = CompanyGateway::query()->find($this->company_gateway_id);
if(!$company_gateway)
return;
$stripe_driver = $company_gateway->driver()->init(); $stripe_driver = $company_gateway->driver()->init();
$charge_id = false; $charge_id = false;

View File

@ -44,7 +44,6 @@ class InstantPayment
public function run() public function run()
{ {
nlog($this->request->all());
$cc = auth()->guard('contact')->user(); $cc = auth()->guard('contact')->user();

View File

@ -248,6 +248,12 @@ class Email implements ShouldQueue
return $this; return $this;
} }
private function incrementEmailCounter(): void
{
if (in_array($this->mailer, ['default', 'mailgun']))
Cache::increment("email_quota" . $this->company->account->key);
}
/** /**
* Attempts to send the email * Attempts to send the email
* *
@ -279,7 +285,7 @@ class Email implements ShouldQueue
$mailer->send($this->mailable); $mailer->send($this->mailable);
Cache::increment("email_quota" . $this->company->account->key); $this->incrementEmailCounter();
LightLogs::create(new EmailSuccess($this->company->company_key, $this->mailable->subject)) LightLogs::create(new EmailSuccess($this->company->company_key, $this->mailable->subject))
->send(); ->send();
@ -501,7 +507,7 @@ class Email implements ShouldQueue
// return $this; // return $this;
// } // }
if(Ninja::isHosted() && $this->company->account->isPaid() && $this->email_object->settings->email_sending_method == 'default') { if (Ninja::isHosted() && $this->company->account->isPaid() && $this->email_object->settings->email_sending_method == 'default') {
try { try {
@ -510,17 +516,24 @@ class Email implements ShouldQueue
$domain = explode("@", $email)[1] ?? ""; $domain = explode("@", $email)[1] ?? "";
$dns = dns_get_record($domain, DNS_MX); $dns = dns_get_record($domain, DNS_MX);
$server = $dns[0]["target"]; $server = $dns[0]["target"];
if(stripos($server, "outlook.com") !== false) { if (stripos($server, "outlook.com") !== false) {
if (property_exists($this->email_object->settings, 'email_from_name') && strlen($this->email_object->settings->email_from_name) > 1) {
$email_from_name = $this->email_object->settings->email_from_name;
} else {
$email_from_name = $this->company->present()->name();
}
$this->mailer = 'postmark'; $this->mailer = 'postmark';
$this->client_postmark_secret = config('services.postmark-outlook.token'); $this->client_postmark_secret = config('services.postmark-outlook.token');
$this->mailable $this->mailable
->from('maildelivery@invoice.services', 'Invoice Ninja'); ->from(config('services.postmark-outlook.from.address'), $email_from_name);
return $this; return $this;
} }
} catch(\Exception $e) { } catch (\Exception $e) {
nlog("problem switching outlook driver - hosted");
nlog($e->getMessage()); nlog($e->getMessage());
} }
} }
@ -551,11 +564,10 @@ class Email implements ShouldQueue
$this->mailer = 'mailgun'; $this->mailer = 'mailgun';
$this->setMailgunMailer(); $this->setMailgunMailer();
return $this; return $this;
case 'client_brevo': case 'smtp':
$this->mailer = 'brevo'; $this->mailer = 'smtp';
$this->setBrevoMailer(); $this->configureSmtpMailer();
return $this; return $this;
default: default:
$this->mailer = config('mail.default'); $this->mailer = config('mail.default');
return $this; return $this;
@ -568,6 +580,43 @@ class Email implements ShouldQueue
return $this; return $this;
} }
private function configureSmtpMailer(): void
{
$company = $this->company;
$smtp_host = $company->smtp_host;
$smtp_port = $company->smtp_port;
$smtp_username = $company->smtp_username;
$smtp_password = $company->smtp_password;
$smtp_encryption = $company->smtp_encryption ?? 'tls';
$smtp_local_domain = strlen($company->smtp_local_domain) > 2 ? $company->smtp_local_domain : null;
$smtp_verify_peer = $company->smtp_verify_peer ?? true;
config([
'mail.mailers.smtp' => [
'transport' => 'smtp',
'host' => $smtp_host,
'port' => $smtp_port,
'username' => $smtp_username,
'password' => $smtp_password,
'encryption' => $smtp_encryption,
'local_domain' => $smtp_local_domain,
'verify_peer' => $smtp_verify_peer,
'timeout' => 30,
],
]);
$user = $this->resolveSendingUser();
$sending_email = (isset ($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email;
$sending_user = (isset ($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name();
$this->mailable
->from($sending_email, $sending_user);
}
/** /**
* Allows configuration of multiple mailers * Allows configuration of multiple mailers
* per company for use by self hosted users * per company for use by self hosted users
@ -668,8 +717,8 @@ class Email implements ShouldQueue
$user = $this->resolveSendingUser(); $user = $this->resolveSendingUser();
$sending_email = (isset($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email; $sending_email = (isset ($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email;
$sending_user = (isset($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name(); $sending_user = (isset ($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name();
$this->mailable $this->mailable
->from($sending_email, $sending_user); ->from($sending_email, $sending_user);
@ -690,8 +739,8 @@ class Email implements ShouldQueue
$user = $this->resolveSendingUser(); $user = $this->resolveSendingUser();
$sending_email = (isset($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email; $sending_email = (isset ($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email;
$sending_user = (isset($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name(); $sending_user = (isset ($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name();
$this->mailable $this->mailable
->from($sending_email, $sending_user); ->from($sending_email, $sending_user);
@ -712,8 +761,8 @@ class Email implements ShouldQueue
$user = $this->resolveSendingUser(); $user = $this->resolveSendingUser();
$sending_email = (isset($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email; $sending_email = (isset ($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email;
$sending_user = (isset($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name(); $sending_user = (isset ($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name();
$this->mailable $this->mailable
->from($sending_email, $sending_user); ->from($sending_email, $sending_user);

View File

@ -107,10 +107,10 @@ class EmailDefaults
match ($this->email->email_object->settings->email_style) { match ($this->email->email_object->settings->email_style) {
'plain' => $this->template = 'email.template.plain', 'plain' => $this->template = 'email.template.plain',
'light' => $this->template = $this->email->email_object->company->account->isPremium() ? 'email.template.client_premium' : 'email.template.client', 'light' => $this->template = 'email.template.client',
'dark' => $this->template = $this->email->email_object->company->account->isPremium() ? 'email.template.client_premium' :'email.template.client', 'dark' => $this->template = 'email.template.client',
'custom' => $this->template = 'email.template.custom', 'custom' => $this->template = 'email.template.custom',
default => $this->template = $this->email->email_object->company->account->isPremium() ? 'email.template.client_premium' :'email.template.client', default => $this->template = 'email.template.client',
}; };
$this->email->email_object->html_template = $this->template; $this->email->email_object->html_template = $this->template;
@ -123,7 +123,7 @@ class EmailDefaults
*/ */
private function setFrom(): self private function setFrom(): self
{ {
if (Ninja::isHosted() && $this->email->email_object->settings->email_sending_method == 'default') { if (Ninja::isHosted() && in_array($this->email->email_object->settings->email_sending_method,['default', 'mailgun'])) {
if ($this->email->company->account->isPaid() && property_exists($this->email->email_object->settings, 'email_from_name') && strlen($this->email->email_object->settings->email_from_name) > 1) { if ($this->email->company->account->isPaid() && property_exists($this->email->email_object->settings, 'email_from_name') && strlen($this->email->email_object->settings->email_from_name) > 1) {
$email_from_name = $this->email->email_object->settings->email_from_name; $email_from_name = $this->email->email_object->settings->email_from_name;
} else { } else {

View File

@ -90,7 +90,7 @@ class InvoiceService
if ($company_currency != $client_currency) { if ($company_currency != $client_currency) {
$exchange_rate = new CurrencyApi(); $exchange_rate = new CurrencyApi();
$this->invoice->exchange_rate = $exchange_rate->exchangeRate($client_currency, $company_currency, now()); $this->invoice->exchange_rate = 1/$exchange_rate->exchangeRate($client_currency, $company_currency, now());
} }
return $this; return $this;

View File

@ -90,6 +90,9 @@ class ARDetailReport extends BaseExport
$query = Invoice::query() $query = Invoice::query()
->withTrashed() ->withTrashed()
->whereHas('client', function ($query){
$query->where('is_deleted', 0);
})
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0) ->where('is_deleted', 0)
->where('balance', '>', 0) ->where('balance', '>', 0)

View File

@ -125,9 +125,9 @@ class ARSummaryReport extends BaseExport
$amount = Invoice::withTrashed() $amount = Invoice::withTrashed()
->where('client_id', $this->client->id) ->where('client_id', $this->client->id)
->where('company_id', $this->client->company_id) ->where('company_id', $this->client->company_id)
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0) ->where('balance', '>', 0)
->where('is_deleted', 0)
->where(function ($query) { ->where(function ($query) {
$query->where('due_date', '>', now()->startOfDay()) $query->where('due_date', '>', now()->startOfDay())
->orWhereNull('due_date'); ->orWhereNull('due_date');

View File

@ -25,6 +25,8 @@ use Illuminate\Support\Facades\App;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Csv\Writer; use League\Csv\Writer;
use function Sentry\continueTrace;
class ProfitLoss class ProfitLoss
{ {
private bool $is_income_billed = true; private bool $is_income_billed = true;
@ -280,27 +282,36 @@ class ProfitLoss
$tax_amount_credit = 0; $tax_amount_credit = 0;
$tax_amount_credit_converted = $tax_amount_credit_converted = 0; $tax_amount_credit_converted = $tax_amount_credit_converted = 0;
$invoice = false;
foreach ($payment->paymentables as $pivot) { foreach ($payment->paymentables as $pivot) {
if ($pivot->paymentable_type == 'invoices') { if ($pivot->paymentable_type == 'invoices') {
$invoice = Invoice::query()->withTrashed()->find($pivot->paymentable_id); $invoice = Invoice::query()->withTrashed()->find($pivot->paymentable_id);
if(!$invoice)
continue;
$pivot_diff = $pivot->amount - $pivot->refunded; $pivot_diff = $pivot->amount - $pivot->refunded;
$amount_payment_paid += $pivot_diff; $amount_payment_paid += $pivot_diff;
$amount_payment_paid_converted += $pivot_diff / ($payment->exchange_rate ?: 1); $amount_payment_paid_converted += $pivot_diff * ($payment->exchange_rate ?: 1);
if ($invoice->amount > 0) { if ($invoice->amount > 0) {
$tax_amount += ($pivot_diff / $invoice->amount) * $invoice->total_taxes; $tax_amount += ($pivot_diff / $invoice->amount) * $invoice->total_taxes;
$tax_amount_converted += (($pivot_diff / $invoice->amount) * $invoice->total_taxes) / $payment->exchange_rate; $tax_amount_converted += (($pivot_diff / $invoice->amount) * $invoice->total_taxes) / $invoice->exchange_rate;
} }
} }
if(!$invoice) {
continue;
}
if ($pivot->paymentable_type == 'credits') { if ($pivot->paymentable_type == 'credits') {
$amount_credit_paid += $pivot->amount - $pivot->refunded; $amount_credit_paid += $pivot->amount - $pivot->refunded;
$amount_credit_paid_converted += $pivot_diff / ($payment->exchange_rate ?: 1); $amount_credit_paid_converted += $pivot_diff * ($payment->exchange_rate ?: 1);
$tax_amount_credit += ($pivot_diff / $invoice->amount) * $invoice->total_taxes; $tax_amount_credit += ($pivot_diff / $invoice->amount) * $invoice->total_taxes;
$tax_amount_credit_converted += (($pivot_diff / $invoice->amount) * $invoice->total_taxes) / $payment->exchange_rate; $tax_amount_credit_converted += (($pivot_diff / $invoice->amount) * $invoice->total_taxes) / $invoice->exchange_rate;
} }
} }
@ -340,6 +351,10 @@ class ProfitLoss
*/ */
public function getCsv() public function getCsv()
{ {
nlog($this->income);
nlog($this->income_taxes);
nlog(array_sum(array_column($this->expense_break_down, 'total')));
MultiDB::setDb($this->company->db); MultiDB::setDb($this->company->db);
App::forgetInstance('translator'); App::forgetInstance('translator');
App::setLocale($this->company->locale()); App::setLocale($this->company->locale());
@ -356,7 +371,7 @@ class ProfitLoss
$csv->insertOne(['--------------------']); $csv->insertOne(['--------------------']);
$csv->insertOne([ctrans('texts.total_revenue'), Number::formatMoney($this->income, $this->company)]); $csv->insertOne([ctrans('texts.total_revenue'). "[".ctrans('texts.tax')." " .ctrans('texts.exclusive'). "]", Number::formatMoney($this->income, $this->company)]);
//total taxes //total taxes
@ -371,12 +386,12 @@ class ProfitLoss
//total expense taxes //total expense taxes
$csv->insertOne(['--------------------']); $csv->insertOne(['--------------------']);
$csv->insertOne([ctrans('texts.total_expenses'), Number::formatMoney(array_sum(array_column($this->expense_break_down, 'total')), $this->company)]); $csv->insertOne([ctrans('texts.total_expenses'). "[".ctrans('texts.tax')." " .ctrans('texts.exclusive'). "]", Number::formatMoney(array_sum(array_column($this->expense_break_down, 'total')), $this->company)]);
$csv->insertOne([ctrans('texts.total_taxes'), Number::formatMoney(array_sum(array_column($this->expense_break_down, 'tax')), $this->company)]); $csv->insertOne([ctrans('texts.total_taxes'), Number::formatMoney(array_sum(array_column($this->expense_break_down, 'tax')), $this->company)]);
$csv->insertOne(['--------------------']); $csv->insertOne(['--------------------']);
$csv->insertOne([ctrans('texts.total_profit'), Number::formatMoney($this->income - $this->income_taxes - array_sum(array_column($this->expense_break_down, 'total')) - array_sum(array_column($this->expense_break_down, 'tax')), $this->company)]); $csv->insertOne([ctrans('texts.total_profit'), Number::formatMoney($this->income - array_sum(array_column($this->expense_break_down, 'total')), $this->company)]);
//net profit //net profit
@ -384,11 +399,25 @@ class ProfitLoss
$csv->insertOne(['']); $csv->insertOne(['']);
$csv->insertOne(['']); $csv->insertOne(['']);
$csv->insertOne(['--------------------']);
$csv->insertOne([ctrans('texts.revenue')]);
$csv->insertOne(['--------------------']);
$csv->insertOne([ctrans('texts.currency'), ctrans('texts.amount'), ctrans('texts.total_taxes')]); $csv->insertOne([ctrans('texts.currency'), ctrans('texts.amount'), ctrans('texts.total_taxes')]);
foreach ($this->foreign_income as $foreign_income) { foreach ($this->foreign_income as $foreign_income) {
$csv->insertOne([$foreign_income['currency'], ($foreign_income['amount'] - $foreign_income['total_taxes']), $foreign_income['total_taxes']]); $csv->insertOne([$foreign_income['currency'], ($foreign_income['amount'] - $foreign_income['total_taxes']), $foreign_income['total_taxes']]);
} }
$csv->insertOne(['']);
$csv->insertOne(['']);
$csv->insertOne(['--------------------']);
$csv->insertOne([ctrans('texts.expenses')]);
$csv->insertOne(['--------------------']);
foreach($this->expenses as $expense){
$csv->insertOne([$expense->currency, ($expense->total - $expense->foreign_tax_amount), $expense->foreign_tax_amount]);
}
return $csv->toString(); return $csv->toString();
} }
@ -421,6 +450,11 @@ class ProfitLoss
private function expenseData() private function expenseData()
{ {
$expenses = Expense::query()->where('company_id', $this->company->id) $expenses = Expense::query()->where('company_id', $this->company->id)
->where(function ($query){
$query->whereNull('client_id')->orWhereHas('client', function ($q){
$q->where('is_deleted', 0);
});
})
->where('is_deleted', 0) ->where('is_deleted', 0)
->withTrashed() ->withTrashed()
->whereBetween('date', [$this->start_date, $this->end_date]) ->whereBetween('date', [$this->start_date, $this->end_date])
@ -428,19 +462,21 @@ class ProfitLoss
$this->expenses = []; $this->expenses = [];
$company_currency_code = $this->company->currency()->code;
foreach ($expenses as $expense) { foreach ($expenses as $expense) {
$map = new \stdClass(); $map = new \stdClass();
$amount = $expense->amount; $expense_tax_total = $this->getTax($expense);
$map->total = $expense->amount; $map->total = $expense->amount;
$map->converted_total = $converted_total = $this->getConvertedTotal($expense->amount, $expense->exchange_rate); $map->converted_total = $converted_total = $this->getConvertedTotal($expense->amount, $expense->exchange_rate); //converted to company currency
$map->tax = $tax = $this->getTax($expense); $map->tax = $tax = $this->getConvertedTotal($expense_tax_total, $expense->exchange_rate); //tax component
$map->net_converted_total = $expense->uses_inclusive_taxes ? ($converted_total - $tax) : $converted_total; $map->net_converted_total = $expense->uses_inclusive_taxes ? ($converted_total - $tax) : $converted_total; //excludes all taxes
$map->category_id = $expense->category_id; $map->category_id = $expense->category_id;
$map->category_name = $expense->category ? $expense->category->name : 'No Category Defined'; $map->category_name = $expense->category ? $expense->category->name : 'No Category Defined';
$map->currency_id = $expense->currency_id ?: $expense->company->settings->currency_id; $map->currency_id = $expense->currency_id ?: $expense->company->settings->currency_id;
$map->currency = $expense->currency ? $expense->currency->code : $company_currency_code;
$map->foreign_tax_amount = $expense_tax_total;
$this->expenses[] = $map; $this->expenses[] = $map;
} }
@ -480,10 +516,6 @@ class ProfitLoss
//is amount tax //is amount tax
if ($expense->calculate_tax_by_amount) { if ($expense->calculate_tax_by_amount) {
nlog($expense->tax_amount1);
nlog($expense->tax_amount2);
nlog($expense->tax_amount3);
return $expense->tax_amount1 + $expense->tax_amount2 + $expense->tax_amount3; return $expense->tax_amount1 + $expense->tax_amount2 + $expense->tax_amount3;
} }

View File

@ -75,8 +75,8 @@ class TaxSummaryReport extends BaseExport
$query = Invoice::query() $query = Invoice::query()
->withTrashed() ->withTrashed()
->whereIn('status_id', [2,3,4])
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->whereIn('status_id', [2,3,4])
->where('is_deleted', 0) ->where('is_deleted', 0)
->orderBy('balance', 'desc'); ->orderBy('balance', 'desc');

View File

@ -11,34 +11,35 @@
namespace App\Services\Scheduler; namespace App\Services\Scheduler;
use App\Export\CSV\ClientExport;
use App\Export\CSV\ContactExport;
use App\Export\CSV\CreditExport;
use App\Export\CSV\DocumentExport;
use App\Export\CSV\ExpenseExport;
use App\Export\CSV\InvoiceExport;
use App\Export\CSV\InvoiceItemExport;
use App\Export\CSV\PaymentExport;
use App\Export\CSV\ProductExport;
use App\Export\CSV\ProductSalesExport;
use App\Export\CSV\QuoteExport;
use App\Export\CSV\QuoteItemExport;
use App\Export\CSV\RecurringInvoiceExport;
use App\Export\CSV\TaskExport;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Mail\DownloadReport;
use App\Models\Client; use App\Models\Client;
use App\Models\Scheduler; use App\Models\Scheduler;
use App\Mail\DownloadReport;
use App\Export\CSV\TaskExport;
use App\Export\CSV\QuoteExport;
use App\Utils\Traits\MakesHash;
use App\Export\CSV\ClientExport;
use App\Export\CSV\CreditExport;
use App\Utils\Traits\MakesDates;
use App\Export\CSV\ContactExport;
use App\Export\CSV\ExpenseExport;
use App\Export\CSV\InvoiceExport;
use App\Export\CSV\PaymentExport;
use App\Export\CSV\ProductExport;
use App\Jobs\Mail\NinjaMailerJob;
use App\Export\CSV\ActivityExport;
use App\Export\CSV\DocumentExport;
use App\Export\CSV\QuoteItemExport;
use App\Services\Report\ProfitLoss;
use App\Jobs\Mail\NinjaMailerObject;
use App\Export\CSV\InvoiceItemExport;
use App\Export\CSV\ProductSalesExport;
use App\Services\Report\ARDetailReport; use App\Services\Report\ARDetailReport;
use App\Services\Report\ARSummaryReport; use App\Services\Report\ARSummaryReport;
use App\Services\Report\ClientBalanceReport;
use App\Services\Report\ClientSalesReport;
use App\Services\Report\ProfitLoss;
use App\Services\Report\TaxSummaryReport;
use App\Services\Report\UserSalesReport; use App\Services\Report\UserSalesReport;
use App\Utils\Traits\MakesDates; use App\Services\Report\TaxSummaryReport;
use App\Utils\Traits\MakesHash; use App\Export\CSV\RecurringInvoiceExport;
use App\Services\Report\ClientSalesReport;
use App\Services\Report\ClientBalanceReport;
class EmailReport class EmailReport
{ {
@ -77,19 +78,33 @@ class EmailReport
'client_sales' => $export = (new ClientSalesReport($this->scheduler->company, $data)), 'client_sales' => $export = (new ClientSalesReport($this->scheduler->company, $data)),
'user_sales' => $export = (new UserSalesReport($this->scheduler->company, $data)), 'user_sales' => $export = (new UserSalesReport($this->scheduler->company, $data)),
'profitloss' => $export = (new ProfitLoss($this->scheduler->company, $data)), 'profitloss' => $export = (new ProfitLoss($this->scheduler->company, $data)),
'activity' => $export = (new ActivityExport($this->scheduler->company, $data)),
'activities' => $export = (new ActivityExport($this->scheduler->company, $data)),
'client' => $export = (new ClientExport($this->scheduler->company, $data)), 'client' => $export = (new ClientExport($this->scheduler->company, $data)),
'clients' => $export = (new ClientExport($this->scheduler->company, $data)),
'client_contact' => $export = (new ContactExport($this->scheduler->company, $data)), 'client_contact' => $export = (new ContactExport($this->scheduler->company, $data)),
'client_contacts' => $export = (new ContactExport($this->scheduler->company, $data)),
'credit' => $export = (new CreditExport($this->scheduler->company, $data)), 'credit' => $export = (new CreditExport($this->scheduler->company, $data)),
'credits' => $export = (new CreditExport($this->scheduler->company, $data)),
'document' => $export = (new DocumentExport($this->scheduler->company, $data)), 'document' => $export = (new DocumentExport($this->scheduler->company, $data)),
'documents' => $export = (new DocumentExport($this->scheduler->company, $data)),
'expense' => $export = (new ExpenseExport($this->scheduler->company, $data)), 'expense' => $export = (new ExpenseExport($this->scheduler->company, $data)),
'expenses' => $export = (new ExpenseExport($this->scheduler->company, $data)),
'invoice' => $export = (new InvoiceExport($this->scheduler->company, $data)), 'invoice' => $export = (new InvoiceExport($this->scheduler->company, $data)),
'invoices' => $export = (new InvoiceExport($this->scheduler->company, $data)),
'invoice_item' => $export = (new InvoiceItemExport($this->scheduler->company, $data)), 'invoice_item' => $export = (new InvoiceItemExport($this->scheduler->company, $data)),
'invoice_items' => $export = (new InvoiceItemExport($this->scheduler->company, $data)),
'quote' => $export = (new QuoteExport($this->scheduler->company, $data)), 'quote' => $export = (new QuoteExport($this->scheduler->company, $data)),
'quotes' => $export = (new QuoteExport($this->scheduler->company, $data)),
'quote_item' => $export = (new QuoteItemExport($this->scheduler->company, $data)), 'quote_item' => $export = (new QuoteItemExport($this->scheduler->company, $data)),
'quote_items' => $export = (new QuoteItemExport($this->scheduler->company, $data)),
'recurring_invoice' => $export = (new RecurringInvoiceExport($this->scheduler->company, $data)), 'recurring_invoice' => $export = (new RecurringInvoiceExport($this->scheduler->company, $data)),
'recurring_invoices' => $export = (new RecurringInvoiceExport($this->scheduler->company, $data)),
'payment' => $export = (new PaymentExport($this->scheduler->company, $data)), 'payment' => $export = (new PaymentExport($this->scheduler->company, $data)),
'payments' => $export = (new PaymentExport($this->scheduler->company, $data)),
'product' => $export = (new ProductExport($this->scheduler->company, $data)), 'product' => $export = (new ProductExport($this->scheduler->company, $data)),
'task' => $export = (new TaskExport($this->scheduler->company, $data)), 'products' => $export = (new ProductExport($this->scheduler->company, $data)),
'tasks' => $export = (new TaskExport($this->scheduler->company, $data)),
default => $export = false, default => $export = false,
}; };

View File

@ -717,7 +717,7 @@ class TemplateService
return collect($payment->refund_meta) return collect($payment->refund_meta)
->map(function ($refund) use ($payment) { ->map(function ($refund) use ($payment) {
$date = \Carbon\Carbon::parse($refund['date'])->addSeconds($payment->client->timezone_offset()); $date = \Carbon\Carbon::parse($refund['date'] ?? $payment->date)->addSeconds($payment->client->timezone_offset());
$date = $this->translateDate($date, $payment->client->date_format(), $payment->client->locale()); $date = $this->translateDate($date, $payment->client->date_format(), $payment->client->locale());
$entity = ctrans('texts.invoice'); $entity = ctrans('texts.invoice');
@ -1032,6 +1032,8 @@ class TemplateService
'payment_balance' => $purchase_order->client->payment_balance, 'payment_balance' => $purchase_order->client->payment_balance,
'credit_balance' => $purchase_order->client->credit_balance, 'credit_balance' => $purchase_order->client->credit_balance,
'vat_number' => $purchase_order->client->vat_number ?? '', 'vat_number' => $purchase_order->client->vat_number ?? '',
'address' => $purchase_order->client->present()->address(),
'shipping_address' => $purchase_order->client->present()->shipping_address(),
] : [], ] : [],
'status_id' => (string)($purchase_order->status_id ?: 1), 'status_id' => (string)($purchase_order->status_id ?: 1),
'status' => PurchaseOrder::stringStatus($purchase_order->status_id ?? 1), 'status' => PurchaseOrder::stringStatus($purchase_order->status_id ?? 1),

View File

@ -132,6 +132,8 @@ class CreditTransformer extends EntityTransformer
'paid_to_date' => (float) $credit->paid_to_date, 'paid_to_date' => (float) $credit->paid_to_date,
'subscription_id' => $this->encodePrimaryKey($credit->subscription_id), 'subscription_id' => $this->encodePrimaryKey($credit->subscription_id),
'invoice_id' => $credit->invoice_id ? $this->encodePrimaryKey($credit->invoice_id) : '', 'invoice_id' => $credit->invoice_id ? $this->encodePrimaryKey($credit->invoice_id) : '',
'tax_info' => $credit->tax_data ?: new \stdClass(),
]; ];
} }
} }

View File

@ -52,6 +52,7 @@ class DocumentTransformer extends EntityTransformer
'created_at' => (int) $document->created_at, 'created_at' => (int) $document->created_at,
'is_deleted' => (bool) false, 'is_deleted' => (bool) false,
'is_public' => (bool) $document->is_public, 'is_public' => (bool) $document->is_public,
'link' => (string) $document->link(),
]; ];
} }
} }

View File

@ -49,6 +49,12 @@ class ProjectTransformer extends EntityTransformer
public function includeClient(Project $project): \League\Fractal\Resource\Item public function includeClient(Project $project): \League\Fractal\Resource\Item
{ {
if (!$project->client) {
nlog("Project {$project->hashed_id} does not have a client attached - this project is in a bad state");
return null;
}
$transformer = new ClientTransformer($this->serializer); $transformer = new ClientTransformer($this->serializer);
return $this->includeItem($project->client, $transformer, Client::class); return $this->includeItem($project->client, $transformer, Client::class);

View File

@ -149,6 +149,7 @@ class PurchaseOrderTransformer extends EntityTransformer
'subscription_id' => $this->encodePrimaryKey($purchase_order->subscription_id), 'subscription_id' => $this->encodePrimaryKey($purchase_order->subscription_id),
'expense_id' => $this->encodePrimaryKey($purchase_order->expense_id), 'expense_id' => $this->encodePrimaryKey($purchase_order->expense_id),
'currency_id' => $purchase_order->currency_id ? (string) $purchase_order->currency_id : '', 'currency_id' => $purchase_order->currency_id ? (string) $purchase_order->currency_id : '',
'tax_info' => $purchase_order->tax_data ?: new \stdClass(),
]; ];
} }
} }

View File

@ -148,7 +148,7 @@ class QuoteTransformer extends EntityTransformer
'paid_to_date' => (float) $quote->paid_to_date, 'paid_to_date' => (float) $quote->paid_to_date,
'project_id' => $this->encodePrimaryKey($quote->project_id), 'project_id' => $this->encodePrimaryKey($quote->project_id),
'subscription_id' => $this->encodePrimaryKey($quote->subscription_id), 'subscription_id' => $this->encodePrimaryKey($quote->subscription_id),
'tax_info' => $quote->tax_data ?: new \stdClass(),
]; ];
} }
} }

View File

@ -397,6 +397,7 @@ class HtmlEngine
$data['$credit.date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.credit_date')]; $data['$credit.date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.credit_date')];
$data['$balance'] = ['value' => Number::formatMoney($this->getBalance(), $this->client) ?: ' ', 'label' => ctrans('texts.balance')]; $data['$balance'] = ['value' => Number::formatMoney($this->getBalance(), $this->client) ?: ' ', 'label' => ctrans('texts.balance')];
$data['$credit.balance'] = ['value' => Number::formatMoney($this->entity_calc->getBalance(), $this->client) ?: ' ', 'label' => ctrans('texts.credit_balance')]; $data['$credit.balance'] = ['value' => Number::formatMoney($this->entity_calc->getBalance(), $this->client) ?: ' ', 'label' => ctrans('texts.credit_balance')];
$data['$client.credit_balance'] = &$data['$credit.balance'];
$data['$invoice.balance'] = &$data['$balance']; $data['$invoice.balance'] = &$data['$balance'];
$data['$taxes'] = ['value' => Number::formatMoney($this->entity_calc->getItemTotalTaxes(), $this->client) ?: ' ', 'label' => ctrans('texts.taxes')]; $data['$taxes'] = ['value' => Number::formatMoney($this->entity_calc->getItemTotalTaxes(), $this->client) ?: ' ', 'label' => ctrans('texts.taxes')];
@ -621,6 +622,33 @@ class HtmlEngine
$data['$task.task3'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task3')]; $data['$task.task3'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task3')];
$data['$task.task4'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task4')]; $data['$task.task4'] = ['value' => '', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'task4')];
if($this->entity->vendor) {
$data['$vendor_name'] = ['value' => $this->entity->vendor->present()->name() ?: '&nbsp;', 'label' => ctrans('texts.vendor_name')];
$data['$vendor.name'] = &$data['$vendor_name'];
$data['$vendor'] = &$data['$vendor_name'];
$data['$vendor.address1'] = ['value' => $this->entity->vendor->address1 ?: '&nbsp;', 'label' => ctrans('texts.address1')];
$data['$vendor.address2'] = ['value' => $this->entity->vendor->address2 ?: '&nbsp;', 'label' => ctrans('texts.address2')];
$data['$vendor.id_number'] = ['value' => $this->entity->vendor->id_number ?: '&nbsp;', 'label' => ctrans('texts.id_number')];
$data['$vendor.number'] = ['value' => $this->entity->vendor->number ?: '&nbsp;', 'label' => ctrans('texts.number')];
$data['$vendor.vat_number'] = ['value' => $this->entity->vendor->vat_number ?: '&nbsp;', 'label' => ctrans('texts.vat_number')];
$data['$vendor.website'] = ['value' => $this->entity->vendor->present()->website() ?: '&nbsp;', 'label' => ctrans('texts.website')];
$data['$vendor.phone'] = ['value' => $this->entity->vendor->present()->phone() ?: '&nbsp;', 'label' => ctrans('texts.phone')];
$data['$vendor.country'] = ['value' => isset($this->entity->vendor->country->name) ? ctrans('texts.country_' . $this->entity->vendor->country->name) : '', 'label' => ctrans('texts.country')];
$data['$vendor.country_2'] = ['value' => isset($this->entity->vendor->country) ? $this->entity->vendor->country->iso_3166_2 : '', 'label' => ctrans('texts.country')];
$data['$vendor_address'] = ['value' => $this->entity->vendor->present()->address() ?: '&nbsp;', 'label' => ctrans('texts.address')];
$data['$vendor.address'] = &$data['$vendor_address'];
$data['$vendor.postal_code'] = ['value' => $this->entity->vendor->postal_code ?: '&nbsp;', 'label' => ctrans('texts.postal_code')];
$data['$vendor.public_notes'] = ['value' => $this->entity->vendor->public_notes ?: '&nbsp;', 'label' => ctrans('texts.notes')];
$data['$vendor.city'] = ['value' => $this->entity->vendor->city ?: '&nbsp;', 'label' => ctrans('texts.city')];
$data['$vendor.state'] = ['value' => $this->entity->vendor->state ?: '&nbsp;', 'label' => ctrans('texts.state')];
$data['$vendor.city_state_postal'] = ['value' => $this->entity->vendor->present()->cityStateZip($this->entity->vendor->city, $this->entity->vendor->state, $this->entity->vendor->postal_code, false) ?: '&nbsp;', 'label' => ctrans('texts.city_state_postal')];
$data['$vendor.postal_city_state'] = ['value' => $this->entity->vendor->present()->cityStateZip($this->entity->vendor->city, $this->entity->vendor->state, $this->entity->vendor->postal_code, true) ?: '&nbsp;', 'label' => ctrans('texts.postal_city_state')];
$data['$vendor.postal_city'] = ['value' => $this->entity->vendor->present()->cityStateZip($this->entity->vendor->city, null, $this->entity->vendor->postal_code, true) ?: '&nbsp;', 'label' => ctrans('texts.postal_city')];
}
if ($this->settings->signature_on_pdf) { if ($this->settings->signature_on_pdf) {
$data['$contact.signature'] = ['value' => $this->invitation->signature_base64, 'label' => ctrans('texts.signature')]; $data['$contact.signature'] = ['value' => $this->invitation->signature_base64, 'label' => ctrans('texts.signature')];
$data['$contact.signature_date'] = ['value' => $this->translateDate($this->invitation->signature_date, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.date')]; $data['$contact.signature_date'] = ['value' => $this->translateDate($this->invitation->signature_date, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.date')];

View File

@ -86,6 +86,33 @@ class Number
return rtrim(rtrim(number_format($value, $precision, $decimal, $thousand), '0'), $decimal); return rtrim(rtrim(number_format($value, $precision, $decimal, $thousand), '0'), $decimal);
} }
public static function parseFloat($value)
{
if(!$value)
return 0;
//remove everything except for numbers, decimals, commas and hyphens
$value = preg_replace('/[^0-9.,-]+/', '', $value);
$decimal = strpos($value, '.');
$comma = strpos($value, ',');
if($comma === false) //no comma must be a decimal number already
return (float) $value;
if($decimal < $comma){ //decimal before a comma = euro
$value = str_replace(['.',','], ['','.'], $value);
return (float) $value;
}
//comma first = traditional thousand separator
$value = str_replace(',', '', $value);
return (float)$value;
}
/** /**
* Formats a given value based on the clients currency * Formats a given value based on the clients currency
* BACK to a float. * BACK to a float.
@ -93,8 +120,9 @@ class Number
* @param string $value The formatted number to be converted back to float * @param string $value The formatted number to be converted back to float
* @return float The formatted value * @return float The formatted value
*/ */
public static function parseFloat($value) public static function parseFloatXX($value)
{ {
if(!$value) if(!$value)
return 0; return 0;
@ -103,17 +131,14 @@ class Number
if(substr($value, 0,1) == '-') if(substr($value, 0,1) == '-')
$multiplier = -1; $multiplier = -1;
// convert "," to "."
$s = str_replace(',', '.', $value); $s = str_replace(',', '.', $value);
// remove everything except numbers and dot "."
$s = preg_replace("/[^0-9\.]/", '', $s); $s = preg_replace("/[^0-9\.]/", '', $s);
if ($s < 1) { if ($s < 1) {
return (float) $s; return (float) $s;
} }
// remove all separators from first part and keep the end
$s = str_replace('.', '', substr($s, 0, -3)).substr($s, -3); $s = str_replace('.', '', substr($s, 0, -3)).substr($s, -3);
if($multiplier) if($multiplier)
@ -122,6 +147,52 @@ class Number
return (float) $s; return (float) $s;
} }
//next iteration of float parsing
public static function parseFloat2($value)
{
if(!$value) {
return 0;
}
//remove everything except for numbers, decimals, commas and hyphens
$value = preg_replace('/[^0-9.,-]+/', '', $value);
$decimal = strpos($value, '.');
$comma = strpos($value, ',');
//check the 3rd last character
if(!in_array(substr($value, -3, 1), [".", ","])) {
if($comma && (substr($value, -3, 1) != ".")) {
$value .= ".00";
} elseif($decimal && (substr($value, -3, 1) != ",")) {
$value .= ",00";
}
}
$decimal = strpos($value, '.');
$comma = strpos($value, ',');
if($comma === false) { //no comma must be a decimal number already
return (float) $value;
}
if($decimal < $comma) { //decimal before a comma = euro
$value = str_replace(['.',','], ['','.'], $value);
return (float) $value;
}
//comma first = traditional thousand separator
$value = str_replace(',', '', $value);
return (float)$value;
}
public static function parseStringFloat($value) public static function parseStringFloat($value)
{ {
$value = preg_replace('/[^0-9-.]+/', '', $value); $value = preg_replace('/[^0-9-.]+/', '', $value);

View File

@ -84,6 +84,7 @@ class SystemHealth
'trailing_slash' => (bool) self::checkUrlState(), 'trailing_slash' => (bool) self::checkUrlState(),
'file_permissions' => (string) self::checkFileSystem(), 'file_permissions' => (string) self::checkFileSystem(),
'exchange_rate_api_not_configured' => (bool)self::checkCurrencySanity(), 'exchange_rate_api_not_configured' => (bool)self::checkCurrencySanity(),
'api_version' => (string) config('ninja.app_version'),
]; ];
} }

833
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,8 +17,8 @@ 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', 'invoicing.co'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => env('APP_VERSION', '5.8.26'), 'app_version' => env('APP_VERSION', '5.8.37'),
'app_tag' => env('APP_TAG', '5.8.26'), 'app_tag' => env('APP_TAG', '5.8.37'),
'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),

View File

@ -39,7 +39,10 @@ return [
], ],
'postmark-outlook' => [ 'postmark-outlook' => [
'token' => env('POSTMARK_OUTLOOK_SECRET','') 'token' => env('POSTMARK_OUTLOOK_SECRET',''),
'from' => [
'address' => env('POSTMARK_OUTLOOK_FROM_ADDRESS', '')
],
], ],
'microsoft' => [ 'microsoft' => [

View File

@ -19,7 +19,7 @@ return new class extends Migration
$table->text('smtp_username')->nullable(); $table->text('smtp_username')->nullable();
$table->text('smtp_password')->nullable(); $table->text('smtp_password')->nullable();
$table->string('smtp_local_domain')->nullable(); $table->string('smtp_local_domain')->nullable();
$table->boolean('smtp_verify_peer')->default(0); $table->boolean('smtp_verify_peer')->default(true);
}); });
} }

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