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()
{
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->saveQuietly();
$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->currency_id = (string)$client->company->settings->currency_id;
$client->settings = $settings;
@ -933,7 +933,6 @@ class CheckData extends Command
});
Invoice::withTrashed()
->where("partial", 0)
->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);
} 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
{
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

View File

@ -36,4 +36,10 @@ interface RuleInterface
public function override($item);
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 = [
FilePermissionsFailure::class,
PDOException::class,
MaxAttemptsExceededException::class,
CommandNotFoundException::class,
ValidationException::class,
ModelNotFoundException::class,
NotFoundHttpException::class,
UnableToCreateDirectory::class,
ConnectException::class,
RuntimeException::class,
InvalidArgumentException::class,
CredentialsException::class,
@ -140,7 +138,7 @@ class Handler extends ExceptionHandler
'email' => 'anonymous@example.com',
'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([
'id' => auth()->user()->account->key,
'email' => 'anonymous@example.com',

View File

@ -826,8 +826,15 @@ class BaseExport
return '';
}
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'])) {
@ -844,8 +851,16 @@ class BaseExport
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)) {
$clients = explode(',', $clients);
@ -862,8 +877,16 @@ class BaseExport
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)) {
@ -878,8 +901,16 @@ class BaseExport
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)) {
@ -894,8 +925,16 @@ class BaseExport
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)) {
@ -911,13 +950,230 @@ class BaseExport
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);
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;
}
@ -942,6 +1198,10 @@ class BaseExport
$invoice_filters[] = Invoice::STATUS_PARTIAL;
}
if (in_array('cancelled', $status_parameters)) {
$invoice_filters[] = Invoice::STATUS_CANCELLED;
}
if (count($invoice_filters) > 0) {
$nested->whereIn('status_id', $invoice_filters);
}
@ -965,15 +1225,19 @@ class BaseExport
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'];
nlog($date_range);
if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key']) > 1) {
$this->date_key = $this->input['date_key'];
}

View File

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

View File

@ -103,10 +103,14 @@ class CreditExport extends BaseExport
->withTrashed()
->with('client')
->where('company_id', $this->company->id)
->where('is_deleted', 0);
->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query);
if($this->input['status'] ?? false) {
$query = $this->addCreditStatusFilter($query, $this->input['status']);
}
if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query);
}
@ -162,6 +166,40 @@ class CreditExport extends BaseExport
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
{
// if (in_array('country_id', $this->input['report_keys'])) {

View File

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

View File

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

View File

@ -71,11 +71,15 @@ class InvoiceItemExport extends BaseExport
->withTrashed()
->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->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) {
$this->queueDocuments($query);
@ -232,9 +236,9 @@ class InvoiceItemExport extends BaseExport
// $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id);
// }
// if (in_array('invoice.recurring_id', $this->input['report_keys'])) {
// $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? '';
// }
if (in_array('invoice.recurring_id', $this->input['report_keys'])) {
$entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? '';
}
if (in_array('invoice.assigned_user_id', $this->input['report_keys'])) {
$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->addPaymentStatusFilters($query, $this->input['status'] ?? '');
if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query);
}

View File

@ -31,51 +31,6 @@ class PurchaseOrderExport extends BaseExport
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)
{
$this->company = $company;
@ -104,10 +59,12 @@ class PurchaseOrderExport extends BaseExport
->withTrashed()
->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->addPurchaseOrderStatusFilter($query, $this->input['status'] ?? '');
if($this->input['document_email_attachment'] ?? false) {
$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)) {
$entity[$key] = $transformed_purchase_order[$parts[1]];
} else {
// nlog($key);
nlog($key);
$entity[$key] = $this->decorator->transform($key, $purchase_order);
// $entity[$key] = '';
@ -182,16 +139,13 @@ class PurchaseOrderExport extends BaseExport
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'])) {
$entity['currency_id'] = $purchase_order->vendor->currency() ? $purchase_order->vendor->currency()->code : $purchase_order->company->currency()->code;
}
if (in_array('vendor_id', $this->input['report_keys'])) {
$entity['vendor'] = $purchase_order->vendor->present()->name();
if (in_array('purchase_order.vendor_id', $this->input['report_keys'])) {
$entity['purchase_order.vendor_id'] = $purchase_order->vendor->present()->name();
}
if (in_array('purchase_order.status', $this->input['report_keys'])) {

View File

@ -63,10 +63,12 @@ class PurchaseOrderItemExport extends BaseExport
$query = PurchaseOrder::query()
->withTrashed()
->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->addPurchaseOrderStatusFilter($query, $this->input['status'] ?? '');
if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query);
}
@ -190,23 +192,35 @@ class PurchaseOrderItemExport extends BaseExport
private function decorateAdvancedFields(PurchaseOrder $purchase_order, array $entity): array
{
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;
// 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;
// }
// 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)) {
$entity['type'] = $purchase_order->typeIdString($entity['type']);
if (in_array('purchase_order.vendor_id', $this->input['report_keys'])) {
$entity['purchase_order.vendor_id'] = $purchase_order->vendor->present()->name();
}
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.status', $this->input['report_keys'])) {
$entity['purchase_order.status'] = $purchase_order->stringStatus($purchase_order->status_id);
}
if (in_array('purchase_order.user_id', $this->input['report_keys'])) {

View File

@ -65,10 +65,12 @@ class QuoteExport extends BaseExport
->withTrashed()
->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->addQuoteStatusFilter($query, $this->input['status'] ?? '');
if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query);
}

View File

@ -66,10 +66,12 @@ class QuoteItemExport extends BaseExport
$query = Quote::query()
->withTrashed()
->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->addQuoteStatusFilter($query, $this->input['status'] ?? '');
if($this->input['document_email_attachment'] ?? false) {
$this->queueDocuments($query);
}

View File

@ -57,10 +57,12 @@ class RecurringInvoiceExport extends BaseExport
->withTrashed()
->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->addRecurringInvoiceStatusFilter($query, $this->input['status'] ?? '');
return $query;
}

View File

@ -68,7 +68,7 @@ class TaskExport extends BaseExport
$query = Task::query()
->withTrashed()
->where('company_id', $this->company->id)
->where('is_deleted', 0);
->where('is_deleted', $this->input['include_deleted'] ?? false);
$query = $this->addDateRange($query);
@ -202,6 +202,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
{

View File

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

View File

@ -79,7 +79,6 @@ class InvoiceDecorator extends Decorator implements DecoratorInterface
return $invoice->partial_due_date ?? '';
}
public function assigned_user_id(Invoice $invoice)
{
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
{
if (strlen($filter) == 0) {
return $this->builder;
}
return $this->builder;
return $this->builder->where('name', 'like', '%'.$filter.'%');
}
/**
@ -47,9 +49,46 @@ class DocumentFilters extends QueryFilters
*/
public function client_id(string $client_id = ''): Builder
{
return $this->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;
}
/**
* Sorts the list based on $sort.
*

View File

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

View File

@ -164,7 +164,7 @@ class PaymentFilters extends QueryFilters
{
$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;
}

View File

@ -50,7 +50,7 @@ class UserFilters extends QueryFilters
{
$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;
}

View File

@ -19,6 +19,7 @@
namespace App\Helpers\Bank\Nordigen;
use App\Models\Company;
use App\Services\Email\Email;
use App\Models\BankIntegration;
use App\Services\Email\EmailObject;
@ -96,11 +97,11 @@ class Nordigen
return $it->transform($out);
} 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
* @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);
$it = new TransactionTransformer();
$it = new TransactionTransformer($company);
return $it->transform($transactionResponse);
}

View File

@ -12,7 +12,10 @@
namespace App\Helpers\Bank\Nordigen\Transformer;
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 Illuminate\Support\Facades\Cache;
use Log;
@ -66,6 +69,13 @@ class TransactionTransformer implements BankRevenueInterface
{
use AppSetup;
private Company $company;
function __construct(Company $company)
{
$this->company = $company;
}
public function transform($transactionResponse)
{
$data = [];
@ -112,8 +122,8 @@ class TransactionTransformer implements BankRevenueInterface
// enrich description with currencyExchange informations
if (isset($transaction['currencyExchange'])) {
foreach ($transaction["currencyExchange"] as $exchangeRate) {
$targetAmount = round($amount * (float) ($exchangeRate["exchangeRate"] ?? 1) , 2);
$description .= '\nexchangeRate: ' . $amount . " " . ($exchangeRate["sourceCurrency"] ?? '?') . " = " . $targetAmount . " " . ($exchangeRate["targetCurrency"] ?? '?') . " (" . ($exchangeRate["quotationDate"] ?? '?') . ")";
$targetAmount = round($amount * (float) ($exchangeRate["exchangeRate"] ?? 1), 2);
$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,10 +177,12 @@ class InvoiceItemSum
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";
$this->rule = new $class();
if($this->rule->regionWithNoTaxCoverage($this->client->country->iso_3166_2)) {
return $this;
}

View File

@ -122,7 +122,7 @@ class InvoiceSum
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->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];
}
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->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];
}
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->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->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 {
$this->invoice->balance = Number::roundValue($this->getTotal(), $this->precision);
}

View File

@ -122,7 +122,7 @@ class InvoiceSumInclusive
private function calculateInvoiceTaxes()
{
$amount = $this->total;
if ($this->invoice->discount > 0 && $this->invoice->is_amount_discount) {
$amount = $this->formatValue(($this->sub_total - $this->invoice->discount), 2);
}
@ -131,20 +131,20 @@ class InvoiceSumInclusive
$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);
$this->total_taxes += $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);
$this->total_taxes += $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);
$this->total_taxes += $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 ($this->invoice->status_id != Invoice::STATUS_DRAFT) {
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 {
$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.
$qrBill->setAdditionalInformation(
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\Models\Expense;
use App\Models\Payment;
use App\Models\Project;
use App\Models\RecurringExpense;
use App\Models\RecurringInvoice;
use App\Models\Task;
use App\Transformers\DocumentTransformer;
@ -421,7 +424,7 @@ class ClientController extends BaseController
$documents = Document::query()
->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);
})
->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}")) {
$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->to_user = $invitation->contact->company->owner();
$nmo->settings = $invitation->contact->company->settings;

View File

@ -271,6 +271,7 @@ class InvitationController extends Controller
->with('contact.client')
->firstOrFail();
if ($invitation->contact->trashed()) {
$invitation->contact->restore();
}
@ -294,7 +295,10 @@ class InvitationController extends Controller
'payable_invoices' => [
['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);

View File

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

View File

@ -12,16 +12,17 @@
namespace App\Http\Controllers\ClientPortal;
use App\Utils\Number;
use App\Utils\HtmlEngine;
use Illuminate\View\View;
use App\DataMapper\InvoiceItem;
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\MakesDates;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\Factory;
use Illuminate\View\View;
use App\Repositories\InvoiceRepository;
use App\Http\Requests\ClientPortal\PrePayments\StorePrePaymentRequest;
/**
* Class PrePaymentController.
@ -88,6 +89,8 @@ class PrePaymentController extends Controller
$total = $invoice->balance;
$invitation = $invoice->invitations->first();
//format totals
$formatted_total = Number::formatMoney($invoice->amount, auth()->guard('contact')->user()->client);
@ -113,6 +116,8 @@ class PrePaymentController extends Controller
'frequency_id' => $request->frequency_id,
'remaining_cycles' => $request->remaining_cycles,
'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);

View File

@ -25,6 +25,10 @@ class SubscriptionPurchaseController extends Controller
{
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 */
if (auth()->guard('contact')->user() && auth()->guard('contact')->user()->company_id != $subscription->company_id) {
auth()->guard('contact')->logout();

View File

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

View File

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

View File

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

View File

@ -30,18 +30,24 @@ class SmtpController extends BaseController
$user = auth()->user();
$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([
'mail.mailers.smtp' => [
'transport' => 'smtp',
'host' => $request->input('smtp_host', $company->smtp_host),
'port' => $request->input('smtp_port', $company->smtp_port),
'username' => $request->input('smtp_username', $company->smtp_username),
'password' => $request->input('smtp_password', $company->smtp_password),
'encryption' => $request->input('smtp_encryption', $company->smtp_encryption ?? 'tls'),
'local_domain' => $request->input('smtp_local_domain', strlen($company->smtp_local_domain) > 2 ? $company->smtp_local_domain : null),
'verify_peer' => $request->input('verify_peer', $company->smtp_verify_peer ?? true),
'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' => 5,
],
]);
@ -49,7 +55,7 @@ class SmtpController extends BaseController
(new \Illuminate\Mail\MailServiceProvider(app()))->register();
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) {
app('mail.manager')->forgetMailers();
return response()->json(['message' => $e->getMessage()], 400);

View File

@ -64,6 +64,8 @@ class StripeConnectController extends BaseController
if ($request->has('error') && $request->error == 'access_denied') {
return view('auth.connect.access_denied');
}
$response = false;
try {
/** @class \stdClass $response
@ -88,6 +90,11 @@ class StripeConnectController extends BaseController
nlog($response);
} catch (\Exception $e) {
}
if(!$response) {
return view('auth.connect.access_denied');
}
@ -144,11 +151,14 @@ class StripeConnectController extends BaseController
if(isset($request->getTokenContent()['is_react']) && $request->getTokenContent()['is_react']) {
$redirect_uri = config('ninja.react_url').'/#/settings/online_payments';
} else {
$redirect_uri = config('ninja.app_url').'/stripe/completed';
$redirect_uri = config('ninja.app_url');
}
\Illuminate\Support\Facades\Cache::pull($request->token);
//response here
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);
} elseif (! $secret || ! $google2fa->verifyKey($secret, $oneTimePassword)) {
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);

View File

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

View File

@ -59,20 +59,6 @@ class StoreClientRequest extends Request
$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*/
//$rules['name'] = 'required|min:1';
$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['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['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;
}
@ -139,12 +128,16 @@ class StoreClientRequest extends Request
if (! array_key_exists('currency_id', $input['settings']) && isset($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;
} else {
$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;
}
@ -160,10 +153,13 @@ class StoreClientRequest extends Request
}
}
// 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']);
}
@ -173,10 +169,14 @@ class StoreClientRequest extends Request
unset($input['number']);
}
// prevent xss injection
if (array_key_exists('name', $input)) {
$input['name'] = strip_tags($input['name']);
}
//If you want to validate, the prop must be set.
$input['id'] = null;
$this->replace($input);
}

View File

@ -60,17 +60,11 @@ class UpdateClientRequest extends Request
$rules['company_logo'] = 'mimes:jpeg,jpg,png,gif|max:10000';
$rules['industry_id'] = 'integer|nullable';
$rules['size_id'] = 'integer|nullable';
$rules['country_id'] = 'integer|nullable';
$rules['shipping_country_id'] = 'integer|nullable';
$rules['country_id'] = 'integer|nullable|exists:countries,id';
$rules['shipping_country_id'] = 'integer|nullable|exists:countries,id';
$rules['classification'] = 'bail|sometimes|nullable|in:individual,business,company,partnership,trust,charity,government,other';
if ($this->id_number) {
$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['id_number'] = ['sometimes', 'bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)->ignore($this->client->id)];
$rules['number'] = ['sometimes', 'bail', Rule::unique('clients')->where('company_id', $user->company()->id)->ignore($this->client->id)];
$rules['settings'] = new ValidClientGroupSettingsRule();
$rules['contacts'] = 'array';
@ -112,6 +106,9 @@ class UpdateClientRequest extends Request
if (array_key_exists('settings', $input) && ! 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;
}
if (isset($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']);
}
// 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);
}
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)
{
$languages = Cache::get('languages');

View File

@ -57,7 +57,7 @@ class StoreCompanyRequest extends Request
}
$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_local_domain'] = 'sometimes|string|nullable';
$rules['smtp_encryption'] = 'sometimes|string|nullable';
@ -96,6 +96,10 @@ class StoreCompanyRequest extends Request
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']))
$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['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_local_domain'] = 'sometimes|string|nullable';
// $rules['smtp_verify_peer'] = 'sometimes|string';
@ -105,6 +105,10 @@ class UpdateCompanyRequest extends Request
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'])) {
$input['smtp_verify_peer'] == 'true' ? true : false;
}

View File

@ -77,6 +77,7 @@ class StoreInvoiceRequest extends Request
$rules['exchange_rate'] = 'bail|sometimes|numeric';
$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['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date'];
return $rules;
@ -112,6 +113,12 @@ class StoreInvoiceRequest extends Request
$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);
}
}

View File

@ -78,6 +78,7 @@ class UpdateInvoiceRequest extends Request
$rules['exchange_rate'] = 'bail|sometimes|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['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date'];
return $rules;
@ -107,6 +108,12 @@ class UpdateInvoiceRequest extends Request
$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);
}

View File

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

View File

@ -37,7 +37,8 @@ class GenericReportRequest extends Request
'start_date' => 'bail|required_if:date_range,custom|nullable|date',
'report_keys' => 'present|array',
'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',
];
}
@ -63,6 +64,8 @@ class GenericReportRequest extends Request
$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;
$this->replace($input);

View File

@ -36,18 +36,46 @@ class CheckSmtpRequest extends Request
public function rules()
{
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()
{
/** @var \App\Models\User $user */
$user = auth()->user();
$company = $user->company();
$input = $this->input();
if(isset($input['smtp_username']) && $input['smtp_username'] == '********')
unset($input['smtp_username']);
if(isset($input['smtp_username']) && $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);
}

View File

@ -13,11 +13,30 @@ namespace App\Http\Requests\TaskScheduler;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Scheduler\ValidClientIds;
use App\Utils\Traits\MakesHash;
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.
*
@ -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.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'],
'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.status' => ['bail','sometimes', 'nullable', 'string'],
];
@ -73,10 +92,18 @@ class StoreSchedulerRequest extends Request
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']))
->filter(function ($status) {
return in_array($status, ['all','draft','paid','unpaid','overdue']);
})->implode(",") ?? '';
return in_array($status, $this->client_statuses);
})->merge($task_statuses)
->implode(",") ?? '';
}
$this->replace($input);

View File

@ -16,6 +16,27 @@ use App\Http\ValidationRules\Scheduler\ValidClientIds;
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.
*
@ -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.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'],
'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.status' => ['bail','sometimes', 'string'],
'parameters.status' => ['bail','sometimes', 'nullable', 'string'],
];
return $rules;
@ -71,10 +92,18 @@ class UpdateSchedulerRequest extends Request
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']))
->filter(function ($status) {
return in_array($status, ['all','draft','paid','unpaid','overdue']);
})->implode(",") ?? '';
return in_array($status, $this->client_statuses);
})->merge($task_statuses)
->implode(",") ?? '';
}
$this->replace($input);

View File

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

View File

@ -23,7 +23,10 @@ class StoreWebhookRequest extends Request
*/
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()
@ -31,7 +34,6 @@ class StoreWebhookRequest extends Request
return [
'target_url' => 'bail|required|url',
'event_id' => 'bail|required',
// 'headers' => 'bail|sometimes|json',
'rest_method' => 'required|in:post,put'
];
}
@ -43,9 +45,7 @@ class StoreWebhookRequest extends Request
if (!isset($input['rest_method'])) {
$input['rest_method'] = 'post';
}
// if(isset($input['headers']) && count($input['headers']) == 0)
// $input['headers'] = null;
$this->replace($input);
}
}

View File

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

View File

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

View File

@ -114,23 +114,26 @@ class ProcessBankTransactionsNordigen implements ShouldQueue
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->save();
$this->stop_loop = false;
nlog("Nordigen: account inactive: " . $this->bank_integration->nordigen_account_id);
// @turbo124 @todo send email for expired account
$this->nordigen->disabledAccountEmail($this->bank_integration);
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->bank_account_status = $this->nordigen_account['account_status'];
$this->bank_integration->balance = $this->nordigen_account['current_balance'];
$this->bank_integration->bank_account_status = $account['account_status'];
$this->bank_integration->balance = $account['current_balance'];
$this->bank_integration->save();
}
@ -138,7 +141,7 @@ class ProcessBankTransactionsNordigen implements ShouldQueue
private function processTransactions()
{
//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 (count($transactions) == 0) {

View File

@ -69,10 +69,9 @@ class CompanyExport implements ShouldQueue
{
MultiDB::setDb($this->company->db);
$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);
@ -114,8 +113,6 @@ class CompanyExport implements ShouldQueue
return $user;
})->all();
$x = $this->writer->collection('users');
$x->addItems($this->export_data['users']);
$this->export_data = null;
@ -667,7 +664,7 @@ class CompanyExport implements ShouldQueue
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();
@ -675,8 +672,8 @@ class CompanyExport implements ShouldQueue
nlog("cannot open {$zip_path}");
}
$zip->addFile($this->file_name);
$zip->renameName($this->file_name, 'backup.json');
$zip->addFile(sys_get_temp_dir().'/'.$this->file_name, 'backup.json');
// $zip->renameName($this->file_name, 'backup.json');
$zip->close();
@ -686,8 +683,8 @@ class CompanyExport implements ShouldQueue
unlink($zip_path);
}
if(file_exists($this->file_name)) {
unlink($this->file_name);
if(file_exists(sys_get_temp_dir().'/'.$this->file_name)) {
unlink(sys_get_temp_dir().'/'.$this->file_name);
}
if(Ninja::isSelfHost()) {
@ -717,8 +714,8 @@ class CompanyExport implements ShouldQueue
if (Ninja::isHosted()) {
sleep(3);
if(file_exists($zip_path)) {
unlink($zip_path);
if(file_exists(sys_get_temp_dir().'/'.$zip_path)) {
unlink(sys_get_temp_dir().'/'.$zip_path);
}
}
}

View File

@ -311,8 +311,11 @@ class CompanyImport implements ShouldQueue
}
}
unlink($tmp_file);
unlink(Storage::path($this->file_location));
if(file_exists($tmp_file))
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);
/* 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))
->send();
@ -226,6 +227,12 @@ class NinjaMailerJob implements ShouldQueue
$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
*
@ -284,13 +291,21 @@ class NinjaMailerJob implements ShouldQueue
$this->mailer = 'postmark';
$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
->mailable
->from('maildelivery@invoice.services', 'Invoice Ninja');
->from(config('services.postmark-outlook.from.address'), $email_from_name);
return $this;
}
} catch(\Exception $e) {
nlog("problem switching outlook driver - hosted");
nlog($e->getMessage());
}
}
@ -321,11 +336,10 @@ class NinjaMailerJob implements ShouldQueue
$this->mailer = 'mailgun';
$this->setMailgunMailer();
return $this;
case 'client_brevo':
$this->mailer = 'brevo';
$this->setBrevoMailer();
case 'smtp':
$this->mailer = 'smtp';
$this->configureSmtpMailer();
return $this;
default:
break;
}
@ -337,6 +351,48 @@ class NinjaMailerJob implements ShouldQueue
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
* per company for use by self hosted users

View File

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

View File

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

View File

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

View File

@ -123,7 +123,7 @@ class WebhookSingle implements ShouldQueue
]);
(new SystemLogger(
array_merge((array) $response, $data),
['message' => $response->getHeaders(), 'body' => $data],
SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_SUCCESS,
SystemLog::TYPE_WEBHOOK_RESPONSE,
@ -136,7 +136,7 @@ class WebhookSingle implements ShouldQueue
nlog($e->getMessage());
(new SystemLogger(
['message' => "Error connecting to ". $subscription->target_url],
['message' => "Error connecting to ". $subscription->target_url, 'body' => $data],
SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE,
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.";
(new SystemLogger(
['message' => $message],
['message' => $message, 'body' => $data],
SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE,
SystemLog::TYPE_WEBHOOK_RESPONSE,
@ -170,7 +170,7 @@ class WebhookSingle implements ShouldQueue
nlog($message);
(new SystemLogger(
['message' => $message],
['message' => $message, 'body' => $data],
SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE,
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.";
(new SystemLogger(
['message' => $message],
['message' => $message, 'body' => $data],
SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE,
SystemLog::TYPE_WEBHOOK_RESPONSE,
@ -208,7 +208,7 @@ class WebhookSingle implements ShouldQueue
$error = json_decode($e->getResponse()->getBody()->getContents());
(new SystemLogger(
['message' => $error],
['message' => $error, 'body' => $data],
SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE,
SystemLog::TYPE_WEBHOOK_RESPONSE,
@ -220,7 +220,7 @@ class WebhookSingle implements ShouldQueue
$error = json_decode($e->getResponse()->getBody()->getContents());
(new SystemLogger(
['message' => $error],
['message' => $error, 'body' => $data],
SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE,
SystemLog::TYPE_WEBHOOK_RESPONSE,
@ -232,7 +232,7 @@ class WebhookSingle implements ShouldQueue
nlog($e->getCode());
(new SystemLogger(
$e->getMessage(),
['message' => $e->getMessage(), 'body' => $data],
SystemLog::CATEGORY_WEBHOOK,
SystemLog::EVENT_WEBHOOK_FAILURE,
SystemLog::TYPE_WEBHOOK_RESPONSE,

View File

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

View File

@ -12,15 +12,17 @@
namespace App\Livewire;
use App\Models\Client;
use App\Models\Invoice;
use Livewire\Component;
use App\Libraries\MultiDB;
use Illuminate\Support\Str;
use App\Models\ClientContact;
use App\Models\CompanyGateway;
use App\Models\Invoice;
use App\Utils\Traits\MakesHash;
use Livewire\Attributes\Computed;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Livewire\Component;
class RequiredClientInfo extends Component
{
@ -31,10 +33,7 @@ class RequiredClientInfo extends Component
*/
public $show_terms = false;
/**
* @var array
*/
public $invoice;
public $invoice_terms;
/**
* @var bool
@ -49,18 +48,40 @@ class RequiredClientInfo extends Component
/**
* @var ClientContact
*/
public $contact;
public $contact_id;
/**
* @var \App\Models\Client
*/
public $client;
public $client_id;
/**
* @var array
*/
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,
* right side is column in database.
@ -113,50 +134,96 @@ class RequiredClientInfo extends Component
];
protected $rules = [
'client.address1' => '',
'client.address2' => '',
'client.city' => '',
'client.state' => '',
'client.postal_code' => '',
'client.country_id' => '',
'client.shipping_address1' => '',
'client.shipping_address2' => '',
'client.shipping_city' => '',
'client.shipping_state' => '',
'client.shipping_postal_code' => '',
'client.shipping_country_id' => '',
'contact.first_name' => '',
'contact.last_name' => '',
'contact.email' => '',
'client.name' => '',
'client.website' => '',
'client.phone' => '',
'client.custom_value1' => '',
'client.custom_value2' => '',
'client.custom_value3' => '',
'client.custom_value4' => '',
// 'client.address1' => '',
// 'client.address2' => '',
// 'client.city' => '',
// 'client.state' => '',
// 'client.postal_code' => '',
// 'client.country_id' => '',
// 'client.shipping_address1' => '',
// 'client.shipping_address2' => '',
// 'client.shipping_city' => '',
// 'client.shipping_state' => '',
// 'client.shipping_postal_code' => '',
// 'client.shipping_country_id' => '',
// 'contact.first_name' => '',
// 'contact.last_name' => '',
// 'contact.email' => '',
// 'client.name' => '',
// 'client.website' => '',
// 'client.phone' => '',
// 'client.custom_value1' => '',
// 'client.custom_value2' => '',
// 'client.custom_value3' => '',
// '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 $company;
public $company_id;
public $company_gateway_id;
public $db;
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->terms_accepted = false;
$this->show_form = true;
$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
@ -164,6 +231,24 @@ class RequiredClientInfo extends Component
: $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()
{
$this->terms_accepted = !$this->terms_accepted;
@ -171,6 +256,10 @@ class RequiredClientInfo extends Component
public function handleSubmit(array $data): bool
{
MultiDB::setDb($this->db);
$contact = ClientContact::withTrashed()->find($this->contact_id);
$rules = [];
collect($this->fields)->map(function ($field) use (&$rules) {
@ -192,7 +281,7 @@ class RequiredClientInfo extends Component
if ($this->updateClientDetails($data)) {
$this->dispatch(
'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.
@ -209,6 +298,11 @@ class RequiredClientInfo extends Component
$client = [];
$contact = [];
MultiDB::setDb($this->db);
$_contact = ClientContact::withTrashed()->find($this->contact_id);
foreach ($data as $field => $value) {
if (Str::startsWith($field, 'client_')) {
$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)
->push();
$client_update = $this->contact->client
$client_update = $_contact->client
->fill($client)
->push();
if ($contact_update && $client_update) {
if ($_contact) {
/** @var \App\Models\CompanyGateway $cg */
$cg = CompanyGateway::find($this->company_gateway_id);
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")) {
$payment_gateway->updateCustomer();
@ -247,11 +364,15 @@ class RequiredClientInfo extends Component
public function checkFields()
{
MultiDB::setDb($this->db);
$_contact = ClientContact::withTrashed()->find($this->contact_id);
foreach ($this->fields as $index => $field) {
$_field = $this->mappings[$field['name']];
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;
} else {
$this->fields[$index]['filled'] = true;
@ -259,7 +380,7 @@ class RequiredClientInfo extends Component
}
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;
} else {
$this->fields[$index]['filled'] = true;
@ -289,14 +410,18 @@ class RequiredClientInfo extends Component
public function handleCopyBilling(): void
{
MultiDB::setDb($this->db);
$_contact = ClientContact::withTrashed()->find($this->contact_id);
$this->dispatch(
'update-shipping-data',
client_shipping_address_line_1: $this->contact->client->address1,
client_shipping_address_line_2: $this->contact->client->address2,
client_shipping_city: $this->contact->client->city,
client_shipping_state: $this->contact->client->state,
client_shipping_postal_code: $this->contact->client->postal_code,
client_shipping_country_id: $this->contact->client->country_id,
client_shipping_address_line_1: $_contact->client->address1,
client_shipping_address_line_2: $_contact->client->address2,
client_shipping_city: $_contact->client->city,
client_shipping_state: $_contact->client->state,
client_shipping_postal_code: $_contact->client->postal_code,
client_shipping_country_id: $_contact->client->country_id,
);
}

View File

@ -52,7 +52,7 @@ class ClientStatement extends Mailable
public function 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',
with: [
'text_body' => $this->data['body'],

View File

@ -75,7 +75,8 @@ class TemplateEmail extends Mailable
$template_name = 'email.template.'.$this->build_email->getTemplate();
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') {

View File

@ -72,7 +72,8 @@ class VendorTemplateEmail extends Mailable
$template_name = 'email.template.'.$this->build_email->getTemplate();
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') {

View File

@ -208,6 +208,29 @@ class Document extends BaseModel
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
{

View File

@ -137,22 +137,22 @@ class Gateway extends StaticModel
case 56:
return [
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::ALIPAY => ['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::SOFORT => ['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', '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::PRZELEWY24 => ['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', '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::BANCONTACT => ['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', '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::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded', 'payment_intent.payment_failed']],
GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']],
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', 'charge.failed', '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', 'charge.failed', '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', 'charge.failed', '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', 'charge.failed', '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', 'charge.failed', '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', 'charge.failed', ]],
];
case 39:
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']]]; //Checkout

View File

@ -355,11 +355,13 @@ class RecurringInvoice extends BaseModel
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;
} 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;
elseif($this->remaining_cycles != 0 && ($this->status_id == self::STATUS_COMPLETED))
return self::STATUS_ACTIVE;
return $this->status_id;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -248,6 +248,12 @@ class Email implements ShouldQueue
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
*
@ -279,7 +285,7 @@ class Email implements ShouldQueue
$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))
->send();
@ -501,7 +507,7 @@ class Email implements ShouldQueue
// 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 {
@ -510,17 +516,24 @@ class Email implements ShouldQueue
$domain = explode("@", $email)[1] ?? "";
$dns = dns_get_record($domain, DNS_MX);
$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->client_postmark_secret = config('services.postmark-outlook.token');
$this->mailable
->from('maildelivery@invoice.services', 'Invoice Ninja');
->from(config('services.postmark-outlook.from.address'), $email_from_name);
return $this;
}
} catch(\Exception $e) {
} catch (\Exception $e) {
nlog("problem switching outlook driver - hosted");
nlog($e->getMessage());
}
}
@ -551,11 +564,10 @@ class Email implements ShouldQueue
$this->mailer = 'mailgun';
$this->setMailgunMailer();
return $this;
case 'client_brevo':
$this->mailer = 'brevo';
$this->setBrevoMailer();
case 'smtp':
$this->mailer = 'smtp';
$this->configureSmtpMailer();
return $this;
default:
$this->mailer = config('mail.default');
return $this;
@ -568,6 +580,43 @@ class Email implements ShouldQueue
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
* per company for use by self hosted users
@ -668,8 +717,8 @@ class Email implements ShouldQueue
$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();
$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);
@ -690,8 +739,8 @@ class Email implements ShouldQueue
$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();
$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);
@ -712,8 +761,8 @@ class Email implements ShouldQueue
$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();
$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);

View File

@ -107,10 +107,10 @@ class EmailDefaults
match ($this->email->email_object->settings->email_style) {
'plain' => $this->template = 'email.template.plain',
'light' => $this->template = $this->email->email_object->company->account->isPremium() ? 'email.template.client_premium' : 'email.template.client',
'dark' => $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 = 'email.template.client',
'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;
@ -123,7 +123,7 @@ class EmailDefaults
*/
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) {
$email_from_name = $this->email->email_object->settings->email_from_name;
} else {

View File

@ -90,7 +90,7 @@ class InvoiceService
if ($company_currency != $client_currency) {
$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;

View File

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

View File

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

View File

@ -25,6 +25,8 @@ use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;
use League\Csv\Writer;
use function Sentry\continueTrace;
class ProfitLoss
{
private bool $is_income_billed = true;
@ -280,27 +282,36 @@ class ProfitLoss
$tax_amount_credit = 0;
$tax_amount_credit_converted = $tax_amount_credit_converted = 0;
$invoice = false;
foreach ($payment->paymentables as $pivot) {
if ($pivot->paymentable_type == 'invoices') {
$invoice = Invoice::query()->withTrashed()->find($pivot->paymentable_id);
if(!$invoice)
continue;
$pivot_diff = $pivot->amount - $pivot->refunded;
$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) {
$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') {
$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_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()
{
nlog($this->income);
nlog($this->income_taxes);
nlog(array_sum(array_column($this->expense_break_down, 'total')));
MultiDB::setDb($this->company->db);
App::forgetInstance('translator');
App::setLocale($this->company->locale());
@ -356,7 +371,7 @@ class ProfitLoss
$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
@ -371,12 +386,12 @@ class ProfitLoss
//total expense taxes
$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(['--------------------']);
$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
@ -384,11 +399,25 @@ class ProfitLoss
$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')]);
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(['']);
$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();
}
@ -421,6 +450,11 @@ class ProfitLoss
private function expenseData()
{
$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)
->withTrashed()
->whereBetween('date', [$this->start_date, $this->end_date])
@ -428,19 +462,21 @@ class ProfitLoss
$this->expenses = [];
$company_currency_code = $this->company->currency()->code;
foreach ($expenses as $expense) {
$map = new \stdClass();
$amount = $expense->amount;
$expense_tax_total = $this->getTax($expense);
$map->total = $expense->amount;
$map->converted_total = $converted_total = $this->getConvertedTotal($expense->amount, $expense->exchange_rate);
$map->tax = $tax = $this->getTax($expense);
$map->net_converted_total = $expense->uses_inclusive_taxes ? ($converted_total - $tax) : $converted_total;
$map->converted_total = $converted_total = $this->getConvertedTotal($expense->amount, $expense->exchange_rate); //converted to company currency
$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; //excludes all taxes
$map->category_id = $expense->category_id;
$map->category_name = $expense->category ? $expense->category->name : 'No Category Defined';
$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;
}
@ -480,10 +516,6 @@ class ProfitLoss
//is amount tax
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;
}

View File

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

View File

@ -11,34 +11,35 @@
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\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\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\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use App\Services\Report\TaxSummaryReport;
use App\Export\CSV\RecurringInvoiceExport;
use App\Services\Report\ClientSalesReport;
use App\Services\Report\ClientBalanceReport;
class EmailReport
{
@ -77,19 +78,33 @@ class EmailReport
'client_sales' => $export = (new ClientSalesReport($this->scheduler->company, $data)),
'user_sales' => $export = (new UserSalesReport($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)),
'clients' => $export = (new ClientExport($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)),
'credits' => $export = (new CreditExport($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)),
'expenses' => $export = (new ExpenseExport($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_items' => $export = (new InvoiceItemExport($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_items' => $export = (new QuoteItemExport($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)),
'payments' => $export = (new PaymentExport($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,
};

View File

@ -717,7 +717,7 @@ class TemplateService
return collect($payment->refund_meta)
->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());
$entity = ctrans('texts.invoice');
@ -1032,6 +1032,8 @@ class TemplateService
'payment_balance' => $purchase_order->client->payment_balance,
'credit_balance' => $purchase_order->client->credit_balance,
'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' => 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,
'subscription_id' => $this->encodePrimaryKey($credit->subscription_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,
'is_deleted' => (bool) false,
'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
{
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);
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),
'expense_id' => $this->encodePrimaryKey($purchase_order->expense_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,
'project_id' => $this->encodePrimaryKey($quote->project_id),
'subscription_id' => $this->encodePrimaryKey($quote->subscription_id),
'tax_info' => $quote->tax_data ?: new \stdClass(),
];
}
}

View File

@ -397,7 +397,8 @@ class HtmlEngine
$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['$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['$taxes'] = ['value' => Number::formatMoney($this->entity_calc->getItemTotalTaxes(), $this->client) ?: ' ', 'label' => ctrans('texts.taxes')];
$data['$invoice.taxes'] = &$data['$taxes'];
@ -621,6 +622,33 @@ class HtmlEngine
$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')];
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) {
$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')];

View File

@ -86,6 +86,33 @@ class Number
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
* BACK to a float.
@ -93,8 +120,9 @@ class Number
* @param string $value The formatted number to be converted back to float
* @return float The formatted value
*/
public static function parseFloat($value)
public static function parseFloatXX($value)
{
if(!$value)
return 0;
@ -103,17 +131,14 @@ class Number
if(substr($value, 0,1) == '-')
$multiplier = -1;
// convert "," to "."
$s = str_replace(',', '.', $value);
// remove everything except numbers and dot "."
$s = preg_replace("/[^0-9\.]/", '', $s);
if ($s < 1) {
return (float) $s;
}
// remove all separators from first part and keep the end
$s = str_replace('.', '', substr($s, 0, -3)).substr($s, -3);
if($multiplier)
@ -122,6 +147,52 @@ class Number
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)
{
$value = preg_replace('/[^0-9-.]+/', '', $value);

View File

@ -84,6 +84,7 @@ class SystemHealth
'trailing_slash' => (bool) self::checkUrlState(),
'file_permissions' => (string) self::checkFileSystem(),
'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),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => env('APP_VERSION', '5.8.26'),
'app_tag' => env('APP_TAG', '5.8.26'),
'app_version' => env('APP_VERSION', '5.8.37'),
'app_tag' => env('APP_TAG', '5.8.37'),
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),

View File

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

View File

@ -19,7 +19,7 @@ return new class extends Migration
$table->text('smtp_username')->nullable();
$table->text('smtp_password')->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