1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-21 00:41:34 +02:00

Merge pull request #8168 from turbo124/v5-develop

Improve caching on seutp.
This commit is contained in:
David Bomba 2023-01-17 15:57:24 +11:00 committed by GitHub
commit 86f343a59d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 530 additions and 152 deletions

119
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,119 @@
# Contributing to CONTRIBUTING.md
First off, thanks for taking the time to contribute!
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions.
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> - Star the project
> - Tweet about it
> - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
- [Improving The Documentation](#improving-the-documentation)
- [Styleguides](#styleguides)
- [Commit Messages](#commit-messages)
- [Join The Project Team](#join-the-project-team)
## Code of Conduct
This project and everyone participating in it is governed by the
[CONTRIBUTING.md Code of Conduct](blob/v5-stable/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable behavior
to <>.
## I Have a Question
> If you want to ask a question, we assume that you have read the available [Documentation](https://invoiceninja.github.io), Searched the [Forum](https://forum.invoiceninja.com), or tried chatting with us on [Slack](https://invoiceninja.slack.com) [generate a slack invite here](http://slack.invoiceninja.com)
Please reserve issues for bugs, general questions posted in the issues section will be closed and redirected to other support venues.
## I Want To Contribute
> ### Legal Notice
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. Please note that you'll need to sign the CLA for any PRs to be accepted
### Reporting Bugs
#### Before Submitting a Bug Report
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://invoiceninja.github.io). If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](issues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
#### How Do I Submit a Good Bug Report?
> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to <>.
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
- Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section.
Once it's filed:
- The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced.
- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution).
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion for CONTRIBUTING.md, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
#### Before Submitting an Enhancement
- Make sure that you are using the latest version.
- Read the [documentation]() carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
#### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](/issues).
- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
- **Explain why this enhancement would be useful** to most CONTRIBUTING.md users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
### Your First Code Contribution
All PRs should be created against the v5-develop branch.
## Attribution
This guide is based on the **contributing.md**. [Make your own](https://contributing.md/)!

View File

@ -1 +1 @@
5.5.55 5.5.56

View File

@ -134,6 +134,9 @@ class CreateAccount extends Command
(new CreateCompanyPaymentTerms($company, $user))->handle(); (new CreateCompanyPaymentTerms($company, $user))->handle();
(new CreateCompanyTaskStatuses($company, $user))->handle(); (new CreateCompanyTaskStatuses($company, $user))->handle();
(new VersionCheck())->handle(); (new VersionCheck())->handle();
$this->warmCache();
} }
private function warmCache() private function warmCache()
@ -142,25 +145,20 @@ class CreateAccount extends Command
$cached_tables = config('ninja.cached_tables'); $cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) { foreach ($cached_tables as $name => $class) {
if (! Cache::has($name)) { if ($name == 'payment_terms') {
// check that the table exists in case the migration is pending $orderBy = 'num_days';
if (! Schema::hasTable((new $class())->getTable())) { } elseif ($name == 'fonts') {
continue; $orderBy = 'sort_order';
} } elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
if ($name == 'payment_terms') { $orderBy = 'name';
$orderBy = 'num_days'; } else {
} elseif ($name == 'fonts') { $orderBy = 'id';
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
} }
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
} }
} }
} }

View File

@ -12,15 +12,16 @@
namespace App\DataMapper; namespace App\DataMapper;
/** /**
* ClientSettings. * BaseSettings.
*/ */
class BaseSettings class BaseSettings
{ {
//@deprecated
public function __construct($obj) public function __construct($obj)
{ {
foreach ($obj as $key => $value) { // foreach ($obj as $key => $value) {
$obj->{$key} = $value; // $obj->{$key} = $value;
} // }
} }
public static function setCasts($obj, $casts) public static function setCasts($obj, $casts)
@ -57,7 +58,4 @@ class BaseSettings
} }
} }
public static function castSingleAttribute($key, $data)
{
}
} }

View File

@ -451,7 +451,11 @@ class CompanySettings extends BaseSettings
public $email_alignment = 'center'; // center , left, right public $email_alignment = 'center'; // center , left, right
public $show_email_footer = true;
public static $casts = [ public static $casts = [
'show_email_footer' => 'bool',
'email_alignment' => 'string', 'email_alignment' => 'string',
'auto_bill_standard_invoices' => 'bool', 'auto_bill_standard_invoices' => 'bool',
'postmark_secret' => 'string', 'postmark_secret' => 'string',
@ -730,8 +734,9 @@ class CompanySettings extends BaseSettings
* and always ensure an up to date class is returned. * and always ensure an up to date class is returned.
* *
* @param $obj * @param $obj
* @deprecated
*/ */
public function __construct($obj) public function __construct()
{ {
// parent::__construct($obj); // parent::__construct($obj);
} }

View File

@ -16,12 +16,10 @@ use App\Exceptions\InternalPDFFailure;
use App\Exceptions\PhantomPDFFailure; use App\Exceptions\PhantomPDFFailure;
use App\Exceptions\StripeConnectFailure; use App\Exceptions\StripeConnectFailure;
use App\Utils\Ninja; use App\Utils\Ninja;
use Exception;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
use Illuminate\Database\Eloquent\RelationNotFoundException; use Illuminate\Database\Eloquent\RelationNotFoundException;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exceptions\ThrottleRequestsException; use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -34,7 +32,6 @@ use Illuminate\Validation\ValidationException;
use PDOException; use PDOException;
use Sentry\Laravel\Integration; use Sentry\Laravel\Integration;
use Sentry\State\Scope; use Sentry\State\Scope;
use Swift_TransportException;
use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\Debug\Exception\FatalThrowableError;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
@ -50,7 +47,6 @@ class Handler extends ExceptionHandler
*/ */
protected $dontReport = [ protected $dontReport = [
PDOException::class, PDOException::class,
//Swift_TransportException::class,
MaxAttemptsExceededException::class, MaxAttemptsExceededException::class,
CommandNotFoundException::class, CommandNotFoundException::class,
ValidationException::class, ValidationException::class,
@ -80,11 +76,10 @@ class Handler extends ExceptionHandler
{ {
if (! Schema::hasTable('accounts')) { if (! Schema::hasTable('accounts')) {
info('account table not found'); info('account table not found');
return; return;
} }
if (Ninja::isHosted() && ! ($exception instanceof ValidationException)) { if (Ninja::isHosted()) {
Integration::configureScope(function (Scope $scope): void { Integration::configureScope(function (Scope $scope): void {
$name = 'hosted@invoiceninja.com'; $name = 'hosted@invoiceninja.com';
@ -105,7 +100,10 @@ class Handler extends ExceptionHandler
]); ]);
}); });
Integration::captureUnhandledException($exception); if ($this->validException($exception)) {
Integration::captureUnhandledException($exception);
}
} elseif (app()->bound('sentry') && $this->shouldReport($exception)) { } elseif (app()->bound('sentry') && $this->shouldReport($exception)) {
Integration::configureScope(function (Scope $scope): void { Integration::configureScope(function (Scope $scope): void {
if (auth()->guard('contact') && auth()->guard('contact')->user() && auth()->guard('contact')->user()->company->account->report_errors) { if (auth()->guard('contact') && auth()->guard('contact')->user() && auth()->guard('contact')->user()->company->account->report_errors) {
@ -180,8 +178,8 @@ class Handler extends ExceptionHandler
return response()->json(['message' => $exception->getMessage()], 500); return response()->json(['message' => $exception->getMessage()], 500);
} elseif ($exception instanceof ThrottleRequestsException && $request->expectsJson()) { } elseif ($exception instanceof ThrottleRequestsException && $request->expectsJson()) {
return response()->json(['message'=>'Too many requests'], 429); return response()->json(['message'=>'Too many requests'], 429);
} elseif ($exception instanceof FatalThrowableError && $request->expectsJson()) { // } elseif ($exception instanceof FatalThrowableError && $request->expectsJson()) {
return response()->json(['message'=>'Fatal error'], 500); // return response()->json(['message'=>'Fatal error'], 500); //@deprecated
} elseif ($exception instanceof AuthorizationException) { } elseif ($exception instanceof AuthorizationException) {
return response()->json(['message'=> $exception->getMessage()], 401); return response()->json(['message'=> $exception->getMessage()], 401);
} elseif ($exception instanceof TokenMismatchException) { } elseif ($exception instanceof TokenMismatchException) {
@ -208,13 +206,6 @@ class Handler extends ExceptionHandler
return response()->json(['message' => $exception->getMessage()], 400); return response()->json(['message' => $exception->getMessage()], 400);
} }
// elseif ($exception instanceof QueryException) {
// return response()->json(['message' => 'We had a problem executing this query. Please retry.'], 500);
// }
return parent::render($request, $exception); return parent::render($request, $exception);
} }

View File

@ -109,6 +109,7 @@ class BaseController extends Controller
'company.bank_integrations', 'company.bank_integrations',
'company.bank_transactions', 'company.bank_transactions',
'company.bank_transaction_rules', 'company.bank_transaction_rules',
'company.task_schedulers',
]; ];
private $mini_load = [ private $mini_load = [
@ -128,6 +129,7 @@ class BaseController extends Controller
'company.subscriptions', 'company.subscriptions',
'company.bank_integrations', 'company.bank_integrations',
'company.bank_transaction_rules', 'company.bank_transaction_rules',
'company.task_schedulers',
]; ];
public function __construct() public function __construct()
@ -465,6 +467,13 @@ class BaseController extends Controller
$query->where('bank_transaction_rules.user_id', $user->id); $query->where('bank_transaction_rules.user_id', $user->id);
} }
}, },
'company.task_schedulers'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if (! $user->isAdmin()) {
$query->where('schedulers.user_id', $user->id);
}
},
] ]
); );
@ -545,6 +554,12 @@ class BaseController extends Controller
$query->where('bank_transaction_rules.user_id', $user->id); $query->where('bank_transaction_rules.user_id', $user->id);
} }
}, },
'company.task_schedulers'=> function ($query) use ($user) {
if (! $user->isAdmin()) {
$query->where('schedulers.user_id', $user->id);
}
},
] ]
); );
@ -803,6 +818,13 @@ class BaseController extends Controller
$query->where('bank_transactions.user_id', $user->id); $query->where('bank_transactions.user_id', $user->id);
} }
}, },
'company.task_schedulers'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at);
if (! $user->isAdmin()) {
$query->where('schedulers.user_id', $user->id);
}
},
] ]
); );

View File

@ -109,16 +109,24 @@ class ClientStatementController extends BaseController
*/ */
public function statement(CreateStatementRequest $request) public function statement(CreateStatementRequest $request)
{ {
$send_email = false;
if($request->has('send_email') && $request->send_email == 'true')
$send_email = true;
$pdf = $request->client()->service()->statement( $pdf = $request->client()->service()->statement(
$request->only(['start_date', 'end_date', 'show_payments_table', 'show_aging_table', 'status']) $request->only(['start_date', 'end_date', 'show_payments_table', 'show_aging_table', 'status']), $send_email
); );
if($send_email)
return response()->json(['message' => ctrans('texts.email_queued')], 200);
if ($pdf) { if ($pdf) {
return response()->streamDownload(function () use ($pdf) { return response()->streamDownload(function () use ($pdf) {
echo $pdf; echo $pdf;
}, ctrans('texts.statement').'.pdf', ['Content-Type' => 'application/pdf']); }, ctrans('texts.statement').'.pdf', ['Content-Type' => 'application/pdf']);
} }
return response()->json(['message' => 'Something went wrong. Please check logs.']); return response()->json(['message' => ctrans('texts.error_title')], 500);
} }
} }

View File

@ -16,7 +16,6 @@ use App\Factory\CreditFactory;
use App\Factory\InvoiceFactory; use App\Factory\InvoiceFactory;
use App\Factory\QuoteFactory; use App\Factory\QuoteFactory;
use App\Factory\RecurringInvoiceFactory; use App\Factory\RecurringInvoiceFactory;
use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Preview\PreviewInvoiceRequest; use App\Http\Requests\Preview\PreviewInvoiceRequest;
use App\Jobs\Util\PreviewPdf; use App\Jobs\Util\PreviewPdf;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
@ -44,7 +43,6 @@ use App\Utils\Traits\MakesInvoiceHtml;
use App\Utils\Traits\Pdf\PageNumbering; use App\Utils\Traits\Pdf\PageNumbering;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
use Turbo124\Beacon\Facades\LightLogs; use Turbo124\Beacon\Facades\LightLogs;
@ -109,8 +107,6 @@ class PreviewController extends BaseController
$class = "App\Models\\$entity"; $class = "App\Models\\$entity";
$pdf_class = "App\Jobs\\$entity\\Create{$entity}Pdf";
$entity_obj = $class::whereId($this->decodePrimaryKey(request()->input('entity_id')))->company()->first(); $entity_obj = $class::whereId($this->decodePrimaryKey(request()->input('entity_id')))->company()->first();
if (! $entity_obj) { if (! $entity_obj) {
@ -214,6 +210,12 @@ class PreviewController extends BaseController
->first(); ->first();
} }
if($request->has('footer') && !$request->filled('footer') && $request->input('entity') == 'recurring_invoice')
$request->merge(['footer' => $company->settings->invoice_footer]);
if($request->has('terms') && !$request->filled('terms') && $request->input('entity') == 'recurring_invoice')
$request->merge(['terms' => $company->settings->invoice_terms]);
$entity_obj = $repo->save($request->all(), $entity_obj); $entity_obj = $repo->save($request->all(), $entity_obj);
if (! $request->has('entity_id')) { if (! $request->has('entity_id')) {
@ -404,8 +406,8 @@ class PreviewController extends BaseController
'user_id' => auth()->user()->id, 'user_id' => auth()->user()->id,
'company_id' => auth()->user()->company()->id, 'company_id' => auth()->user()->company()->id,
'client_id' => $client->id, 'client_id' => $client->id,
'terms' => 'Sample Terms', 'terms' => auth()->user()->company()->settings->invoice_terms,
'footer' => 'Sample Footer', 'footer' => auth()->user()->company()->settings->invoice_footer,
'public_notes' => 'Sample Public Notes', 'public_notes' => 'Sample Public Notes',
]); ]);

View File

@ -16,7 +16,6 @@ use App\Models\Client;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\AppSetup; use App\Utils\Traits\AppSetup;
use App\Utils\Traits\ClientGroupSettingsSaver; use App\Utils\Traits\ClientGroupSettingsSaver;
use Beganovich\Snappdf\Snappdf;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@ -108,6 +107,8 @@ class SelfUpdateController extends BaseController
$zipFile->openFile($file); $zipFile->openFile($file);
$zipFile->deleteFromName(".htaccess");
$zipFile->extractTo(base_path()); $zipFile->extractTo(base_path());
$zipFile->close(); $zipFile->close();

View File

@ -156,7 +156,11 @@ class PaymentEmailEngine extends BaseEmailEngine
$data['$payment2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'payment2', $this->payment->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment2')]; $data['$payment2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'payment2', $this->payment->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment2')];
$data['$payment3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'payment3', $this->payment->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment3')]; $data['$payment3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'payment3', $this->payment->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment3')];
$data['$payment4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'payment4', $this->payment->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment4')]; $data['$payment4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'payment4', $this->payment->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment4')];
// $data['$type'] = ['value' => $this->payment->type->name ?: '', 'label' => ctrans('texts.payment_type')];
$data['$custom1'] = &$data['$payment1'];
$data['$custom2'] = &$data['$payment2'];
$data['$custom3'] = &$data['$payment3'];
$data['$custom4'] = &$data['$payment4'];
$data['$client1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'client1', $this->client->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'client1')]; $data['$client1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'client1', $this->client->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'client1')];
$data['$client2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'client2', $this->client->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'client2')]; $data['$client2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'client2', $this->client->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'client2')];

View File

@ -12,15 +12,12 @@
namespace App\Models; namespace App\Models;
use App\DataMapper\ClientSettings; use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\Jobs\Entity\CreateEntityPdf;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\UserSessionAttributes; use App\Utils\Traits\UserSessionAttributes;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -115,17 +112,17 @@ class BaseModel extends Model
* reference to the parent class. * reference to the parent class.
* *
* @param $key The key of property * @param $key The key of property
* @return * @deprecated
*/ */
public function getSettingsByKey($key) // public function getSettingsByKey($key)
{ // {
/* Does Setting Exist @ client level */ // /* Does Setting Exist @ client level */
if (isset($this->getSettings()->{$key})) { // if (isset($this->getSettings()->{$key})) {
return $this->getSettings()->{$key}; // return $this->getSettings()->{$key};
} else { // } else {
return (new CompanySettings($this->company->settings))->{$key}; // return (new CompanySettings($this->company->settings))->{$key};
} // }
} // }
public function setSettingsByEntity($entity, $settings) public function setSettingsByEntity($entity, $settings)
{ {

View File

@ -172,6 +172,16 @@ class Company extends BaseModel
return $this->morphMany(Document::class, 'documentable'); return $this->morphMany(Document::class, 'documentable');
} }
public function schedulers()
{
return $this->hasMany(Scheduler::class);
}
public function task_schedulers() //alias for schedulers
{
return $this->hasMany(Scheduler::class);
}
public function all_documents() public function all_documents()
{ {
return $this->hasMany(Document::class); return $this->hasMany(Document::class);

View File

@ -15,23 +15,28 @@ use App\Models\Client;
use App\Models\Credit; use App\Models\Credit;
use App\Services\Client\Merge; use App\Services\Client\Merge;
use App\Services\Client\PaymentMethod; use App\Services\Client\PaymentMethod;
use App\Services\Email\EmailObject;
use App\Services\Email\EmailService;
use App\Utils\Number; use App\Utils\Number;
use Illuminate\Database\Eloquent\Collection; use App\Utils\Traits\MakesDates;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Support\Facades\DB;
class ClientService class ClientService
{ {
private $client; use MakesDates;
public function __construct(Client $client) private string $client_start_date;
{
$this->client = $client; private string $client_end_date;
}
public function __construct(private Client $client){}
public function updateBalance(float $amount) public function updateBalance(float $amount)
{ {
try { try {
\DB::connection(config('database.default'))->transaction(function () use($amount) { DB::connection(config('database.default'))->transaction(function () use($amount) {
$this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first(); $this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first();
$this->client->balance += $amount; $this->client->balance += $amount;
@ -51,7 +56,7 @@ class ClientService
{ {
try { try {
\DB::connection(config('database.default'))->transaction(function () use($balance, $paid_to_date) { DB::connection(config('database.default'))->transaction(function () use($balance, $paid_to_date) {
$this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first(); $this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first();
$this->client->balance += $balance; $this->client->balance += $balance;
@ -64,16 +69,14 @@ class ClientService
nlog("DB ERROR " . $throwable->getMessage()); nlog("DB ERROR " . $throwable->getMessage());
} }
return $this; return $this;
} }
public function updatePaidToDate(float $amount) public function updatePaidToDate(float $amount)
{ {
// $this->client->paid_to_date += $amount;
\DB::connection(config('database.default'))->transaction(function () use($amount) { DB::connection(config('database.default'))->transaction(function () use($amount) {
$this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first(); $this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first();
$this->client->paid_to_date += $amount; $this->client->paid_to_date += $amount;
@ -82,17 +85,21 @@ class ClientService
}, 1); }, 1);
return $this; return $this;
} }
public function adjustCreditBalance(float $amount) public function adjustCreditBalance(float $amount)
{ {
$this->client->credit_balance += $amount; $this->client->credit_balance += $amount;
return $this; return $this;
} }
public function getCreditBalance() :float public function getCreditBalance() :float
{ {
$credits = Credit::withTrashed()->where('client_id', $this->client->id) $credits = Credit::withTrashed()->where('client_id', $this->client->id)
->where('is_deleted', false) ->where('is_deleted', false)
->where(function ($query) { ->where(function ($query) {
@ -102,6 +109,7 @@ class ClientService
->orderBy('created_at', 'ASC'); ->orderBy('created_at', 'ASC');
return Number::roundValue($credits->sum('balance'), $this->client->currency()->precision); return Number::roundValue($credits->sum('balance'), $this->client->currency()->precision);
} }
public function getCredits() public function getCredits()
@ -132,12 +140,71 @@ class ClientService
* Generate the client statement. * Generate the client statement.
* *
* @param array $options * @param array $options
* @param bool $send_email determines if we should send this statement direct to the client
*/ */
public function statement(array $options = []) public function statement(array $options = [], bool $send_email = false)
{ {
return (new Statement($this->client, $options))->run(); $statement = (new Statement($this->client, $options));
$pdf = $statement->run();
if($send_email)
return $this->emailStatement($pdf, $statement->options);
return $pdf;
} }
/**
* Emails the statement to the client
*
* @param mixed $pdf The pdf blob
* @param array $options The statement options array
* @return void
*/
private function emailStatement($pdf, array $options): void
{
$this->client_start_date = $this->translateDate($options['start_date'], $this->client->date_format(), $this->client->locale());
$this->client_end_date = $this->translateDate($options['end_date'], $this->client->date_format(), $this->client->locale());
$email_service = new EmailService($this->buildStatementMailableData($pdf), $this->client->company);
$email_service->send();
}
/**
* Builds and returns an EmailObject for Client Statements
*
* @param mixed $pdf The PDF to send
* @return EmailObject The EmailObject to send
*/
public function buildStatementMailableData($pdf) :EmailObject
{
$email_object = new EmailObject;
$email_object->to = [new Address($this->client->present()->email(), $this->client->present()->name())];
$email_object->attachments = [['file' => base64_encode($pdf), 'name' => ctrans('texts.statement') . ".pdf"]];
$email_object->settings = $this->client->getMergedSettings();
$email_object->company = $this->client->company;
$email_object->client = $this->client;
$email_object->email_template_subject = 'email_subject_statement';
$email_object->email_template_body = 'email_template_statement';
$email_object->variables = [
'$client' => $this->client->present()->name(),
'$start_date' => $this->client_start_date,
'$end_date' => $this->client_end_date,
];
return $email_object;
}
/**
* Saves the client instance
*
* @return Client The Client Model
*/
public function save() :Client public function save() :Client
{ {
$this->client->save(); $this->client->save();

View File

@ -32,25 +32,16 @@ class Statement
{ {
use PdfMakerTrait; use PdfMakerTrait;
protected Client $client;
/** /**
* @var Invoice|Payment|null * @var Invoice|Payment|null
*/ */
protected $entity; protected $entity;
protected array $options;
protected bool $rollback = false; protected bool $rollback = false;
public function __construct(Client $client, array $options) public function __construct(protected Client $client, public array $options){}
{
$this->client = $client;
$this->options = $options; public function run() :?string
}
public function run(): ?string
{ {
$this $this
->setupOptions() ->setupOptions()
@ -110,7 +101,7 @@ class Statement
$maker = null; $maker = null;
$state = null; $state = null;
return $pdf; return $pdf;
} }

View File

@ -89,24 +89,27 @@ class EmailService
if($this->company->is_disabled && !$this->override) if($this->company->is_disabled && !$this->override)
return true; return true;
if(Ninja::isSelfHost())
return false;
/* To handle spam users we drop all emails from flagged accounts */ /* To handle spam users we drop all emails from flagged accounts */
if(Ninja::isHosted() && $this->company->account && $this->company->account->is_flagged) if($this->company->account && $this->company->account->is_flagged)
return true; return true;
/* On the hosted platform we set default contacts a @example.com email address - we shouldn't send emails to these types of addresses */ /* On the hosted platform we set default contacts a @example.com email address - we shouldn't send emails to these types of addresses */
if(Ninja::isHosted() && $this->hasValidEmails()) if($this->hasInValidEmails())
return true; return true;
/* GMail users are uncapped */ /* GMail users are uncapped */
if(Ninja::isHosted() && in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun'])) if(in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun']))
return false; return false;
/* On the hosted platform, if the user is over the email quotas, we do not send the email. */ /* On the hosted platform, if the user is over the email quotas, we do not send the email. */
if(Ninja::isHosted() && $this->company->account && $this->company->account->emailQuotaExceeded()) if($this->company->account && $this->company->account->emailQuotaExceeded())
return true; return true;
/* If the account is verified, we allow emails to flow */ /* If the account is verified, we allow emails to flow */
if(Ninja::isHosted() && $this->company->account && $this->company->account->is_verified_account) { if($this->company->account && $this->company->account->is_verified_account) {
//11-01-2022 //11-01-2022
@ -118,7 +121,7 @@ class EmailService
} }
/* On the hosted platform if the user has not verified their account we fail here - but still check what they are trying to send! */ /* On the hosted platform if the user has not verified their account we fail here - but still check what they are trying to send! */
if(Ninja::isHosted() && $this->company->account && !$this->company->account->account_sms_verified){ if($this->company->account && !$this->company->account->account_sms_verified){
if(class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class)) if(class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class))
return (new \Modules\Admin\Jobs\Account\EmailFilter($this->email_object, $this->company))->run(); return (new \Modules\Admin\Jobs\Account\EmailFilter($this->email_object, $this->company))->run();
@ -133,7 +136,7 @@ class EmailService
return false; return false;
} }
private function hasValidEmails(): bool private function hasInValidEmails(): bool
{ {
foreach($this->email_object->to as $address_object) foreach($this->email_object->to as $address_object)
@ -145,6 +148,8 @@ class EmailService
if(!str_contains($address_object->address, "@")) if(!str_contains($address_object->address, "@"))
return true; return true;
if($address_object->address == " ")
return true;
} }

View File

@ -11,18 +11,10 @@
namespace App\Services\Scheduler; namespace App\Services\Scheduler;
use App\DataMapper\EmailTemplateDefaults;
use App\Mail\Client\ClientStatement;
use App\Models\Client; use App\Models\Client;
use App\Models\Scheduler; use App\Models\Scheduler;
use App\Services\Email\EmailMailable;
use App\Services\Email\EmailObject;
use App\Services\Email\EmailService;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Support\Str;
class SchedulerService class SchedulerService
{ {
@ -54,7 +46,6 @@ class SchedulerService
if(count($this->scheduler->parameters['clients']) >= 1) if(count($this->scheduler->parameters['clients']) >= 1)
$query->where('id', $this->transformKeys($this->scheduler->parameters['clients'])); $query->where('id', $this->transformKeys($this->scheduler->parameters['clients']));
$query->cursor() $query->cursor()
->each(function ($_client){ ->each(function ($_client){
@ -62,10 +53,7 @@ class SchedulerService
$statement_properties = $this->calculateStatementProperties(); $statement_properties = $this->calculateStatementProperties();
//work out the date range //work out the date range
$pdf = $_client->service()->statement($statement_properties); $pdf = $_client->service()->statement($statement_properties,true);
$email_service = new EmailService($this->buildMailableData($pdf), $_client->company);
$email_service->send();
//calculate next run dates; //calculate next run dates;
@ -77,9 +65,6 @@ class SchedulerService
{ {
$start_end = $this->calculateStartAndEndDates(); $start_end = $this->calculateStartAndEndDates();
$this->client_start_date = $this->translateDate($start_end[0], $this->client->date_format(), $this->client->locale());
$this->client_end_date = $this->translateDate($start_end[1], $this->client->date_format(), $this->client->locale());
return [ return [
'start_date' =>$start_end[0], 'start_date' =>$start_end[0],
'end_date' =>$start_end[1], 'end_date' =>$start_end[1],
@ -104,26 +89,7 @@ class SchedulerService
}; };
} }
private function buildMailableData($pdf)
{
$email_object = new EmailObject;
$email_object->to = [new Address($this->client->present()->email(), $this->client->present()->name())];
$email_object->attachments = [['file' => base64_encode($pdf), 'name' => ctrans('texts.statement') . ".pdf"]];
$email_object->settings = $this->client->getMergedSettings();
$email_object->company = $this->client->company;
$email_object->client = $this->client;
$email_object->email_template_subject = 'email_subject_statement';
$email_object->email_template_body = 'email_template_statement';
$email_object->variables = [
'$client' => $this->client->present()->name(),
'$start_date' => $this->client_start_date,
'$end_date' => $this->client_end_date,
];
return $email_object;
}
} }

View File

@ -35,6 +35,7 @@ use App\Models\PurchaseOrder;
use App\Models\Quote; use App\Models\Quote;
use App\Models\RecurringExpense; use App\Models\RecurringExpense;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Models\Scheduler;
use App\Models\Subscription; use App\Models\Subscription;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\Models\Task; use App\Models\Task;
@ -106,6 +107,8 @@ class CompanyTransformer extends EntityTransformer
'bank_integrations', 'bank_integrations',
'bank_transactions', 'bank_transactions',
'bank_transaction_rules', 'bank_transaction_rules',
'task_schedulers',
'schedulers',
]; ];
/** /**
@ -233,6 +236,7 @@ class CompanyTransformer extends EntityTransformer
return $this->includeCollection($company->tokens, $transformer, CompanyToken::class); return $this->includeCollection($company->tokens, $transformer, CompanyToken::class);
} }
public function includeBankTransactions(Company $company) public function includeBankTransactions(Company $company)
{ {
$transformer = new BankTransactionTransformer($this->serializer); $transformer = new BankTransactionTransformer($this->serializer);
@ -241,6 +245,20 @@ class CompanyTransformer extends EntityTransformer
} }
public function includeTaskSchedulers(Company $company)
{
$transformer = new SchedulerTransformer($this->serializer);
return $this->includeCollection($company->schedulers, $transformer, Scheduler::class);
}
public function includeSchedulers(Company $company)
{
$transformer = new SchedulerTransformer($this->serializer);
return $this->includeCollection($company->schedulers, $transformer, Scheduler::class);
}
public function includeBankTransactionRules(Company $company) public function includeBankTransactionRules(Company $company)
{ {
$transformer = new BankTransactionRuleTransformer($this->serializer); $transformer = new BankTransactionRuleTransformer($this->serializer);

View File

@ -20,12 +20,10 @@ use App\Models\GatewayType;
use App\Models\InvoiceInvitation; use App\Models\InvoiceInvitation;
use App\Models\QuoteInvitation; use App\Models\QuoteInvitation;
use App\Models\RecurringInvoiceInvitation; use App\Models\RecurringInvoiceInvitation;
use App\Services\PdfMaker\Designs\Utilities\DesignHelpers;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\AppSetup; use App\Utils\Traits\AppSetup;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use App\Utils\transformTranslations;
use Exception; use Exception;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
@ -165,6 +163,16 @@ class HtmlEngine
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')]; $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.invoice_date')]; $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.invoice_date')];
$data['$invoice.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')];
$data['$invoice.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')];
$data['$invoice.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')];
$data['$invoice.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')];
$data['$custom1'] = &$data['$invoice.custom1'];
$data['$custom2'] = &$data['$invoice.custom2'];
$data['$custom3'] = &$data['$invoice.custom3'];
$data['$custom4'] = &$data['$invoice.custom4'];
if($this->entity->project) { if($this->entity->project) {
$data['$project.name'] = ['value' => $this->entity->project->name, 'label' => ctrans('texts.project')]; $data['$project.name'] = ['value' => $this->entity->project->name, 'label' => ctrans('texts.project')];
$data['$invoice.project'] = &$data['$project.name']; $data['$invoice.project'] = &$data['$project.name'];
@ -204,6 +212,16 @@ class HtmlEngine
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_quote')]; $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_quote')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.quote_date')]; $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.quote_date')];
$data['$quote.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')];
$data['$quote.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')];
$data['$quote.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')];
$data['$quote.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')];
$data['$custom1'] = &$data['$quote.custom1'];
$data['$custom2'] = &$data['$quote.custom2'];
$data['$custom3'] = &$data['$quote.custom3'];
$data['$custom4'] = &$data['$quote.custom4'];
if($this->entity->project) { if($this->entity->project) {
$data['$project.name'] = ['value' => $this->entity->project->name, 'label' => ctrans('texts.project_name')]; $data['$project.name'] = ['value' => $this->entity->project->name, 'label' => ctrans('texts.project_name')];
$data['$invoice.project'] = &$data['$project.name']; $data['$invoice.project'] = &$data['$project.name'];
@ -234,6 +252,11 @@ class HtmlEngine
$data['$credit.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'credit3', $this->entity->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')]; $data['$credit.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'credit3', $this->entity->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')];
$data['$credit.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'credit4', $this->entity->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')]; $data['$credit.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'credit4', $this->entity->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')];
$data['$custom1'] = &$data['$credit.custom1'];
$data['$custom2'] = &$data['$credit.custom2'];
$data['$custom3'] = &$data['$credit.custom3'];
$data['$custom4'] = &$data['$credit.custom4'];
} }
$data['$portal_url'] = ['value' => $this->invitation->getPortalLink(), 'label' =>'']; $data['$portal_url'] = ['value' => $this->invitation->getPortalLink(), 'label' =>''];
@ -251,6 +274,7 @@ class HtmlEngine
$data['$invoice.subtotal'] = &$data['$subtotal']; $data['$invoice.subtotal'] = &$data['$subtotal'];
/* Do not change the order of these */
if ($this->entity->partial > 0) { if ($this->entity->partial > 0) {
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.partial_due')]; $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.partial_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')]; $data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')];
@ -287,10 +311,8 @@ class HtmlEngine
$data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')]; $data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')];
} }
// $data['$amount_in_words'] = ['value' => (new \NumberFormatter($this->client->locale(), \NumberFormatter::SPELLOUT))->format($this->entity->amount), 'label' => '']; /* Do not change the order of these */
// $data['$balance_in_words'] = ['value' => (new \NumberFormatter($this->client->locale(), \NumberFormatter::SPELLOUT))->format($this->entity->balance), 'label' => ''];
// $data['$balance_due'] = $data['$balance_due'];
$data['$outstanding'] = &$data['$balance_due']; $data['$outstanding'] = &$data['$balance_due'];
$data['$partial_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.partial_due')]; $data['$partial_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.partial_due')];
$data['$partial'] = &$data['$partial_due']; $data['$partial'] = &$data['$partial_due'];
@ -323,10 +345,7 @@ class HtmlEngine
$data['$user_iban'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')]; $data['$user_iban'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')];
$data['$invoice.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')];
$data['$invoice.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')];
$data['$invoice.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')];
$data['$invoice.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')];
$data['$invoice.public_notes'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->public_notes ?: ''), $this->client) ?: '', 'label' => ctrans('texts.public_notes')]; $data['$invoice.public_notes'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->public_notes ?: ''), $this->client) ?: '', 'label' => ctrans('texts.public_notes')];
$data['$entity.public_notes'] = &$data['$invoice.public_notes']; $data['$entity.public_notes'] = &$data['$invoice.public_notes'];
$data['$public_notes'] = &$data['$invoice.public_notes']; $data['$public_notes'] = &$data['$invoice.public_notes'];
@ -345,10 +364,7 @@ class HtmlEngine
$data['$valid_until'] = &$data['$quote.valid_until']; $data['$valid_until'] = &$data['$quote.valid_until'];
$data['$credit_amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_amount')]; $data['$credit_amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_amount')];
$data['$credit_balance'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_balance')]; $data['$credit_balance'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_balance')];
$data['$quote.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')];
$data['$quote.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')];
$data['$quote.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')];
$data['$quote.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')];
$data['$credit_number'] = &$data['$number']; $data['$credit_number'] = &$data['$number'];
$data['$credit_no'] = &$data['$number']; $data['$credit_no'] = &$data['$number'];

View File

@ -51,7 +51,12 @@ trait SubscriptionHooker
$error = json_decode($e->getResponse()->getBody()->getContents()); $error = json_decode($e->getResponse()->getBody()->getContents());
if(property_exists($error, 'message')) if(is_null($error)){
nlog("empty response");
nlog($e->getMessage());
}
if($error && property_exists($error, 'message'))
$message = $error->message; $message = $error->message;
return array_merge($body, ['message' => $message, 'status_code' => 500]); return array_merge($body, ['message' => $message, 'status_code' => 500]);

View File

@ -228,6 +228,11 @@ class VendorHtmlEngine
$data['$purchase_order.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')]; $data['$purchase_order.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')];
$data['$purchase_order.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')]; $data['$purchase_order.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')];
$data['$custom1'] = &$data['$purchase_order.custom1'];
$data['$custom2'] = &$data['$purchase_order.custom2'];
$data['$custom3'] = &$data['$purchase_order.custom3'];
$data['$custom4'] = &$data['$purchase_order.custom4'];
$data['$vendor1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor1', $this->vendor->custom_value1, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor1')]; $data['$vendor1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor1', $this->vendor->custom_value1, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor1')];
$data['$vendor2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor2', $this->vendor->custom_value2, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor2')]; $data['$vendor2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor2', $this->vendor->custom_value2, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor2')];
$data['$vendor3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor3', $this->vendor->custom_value3, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor3')]; $data['$vendor3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor3', $this->vendor->custom_value3, $this->company) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor3')];

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.5.55', 'app_version' => '5.5.56',
'app_tag' => '5.5.55', 'app_tag' => '5.5.56',
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''), 'api_secret' => env('API_SECRET', ''),

View File

@ -4924,7 +4924,8 @@ $LANG = array(
'action_add_to_invoice' => 'Add To Invoice', 'action_add_to_invoice' => 'Add To Invoice',
'danger_zone' => 'Danger Zone', 'danger_zone' => 'Danger Zone',
'import_completed' => 'Import completed', 'import_completed' => 'Import completed',
'client_statement_body' => 'Your statement from :start_date to :end_date is attached.' 'client_statement_body' => 'Your statement from :start_date to :end_date is attached.',
'email_queued' => 'Email queued',
); );

View File

@ -180,7 +180,7 @@
</p> </p>
@endisset @endisset
@if(isset($company) && $company instanceof \App\Models\Company) @if(isset($company) && $company instanceof \App\Models\Company && $company->getSetting('show_email_footer'))
<p style="font-size: 15px; color: #2e2e2e; font-family: 'roboto', Arial, Helvetica, sans-serif; font-weight: 500; margin-bottom:0;"> <p style="font-size: 15px; color: #2e2e2e; font-family: 'roboto', Arial, Helvetica, sans-serif; font-weight: 500; margin-bottom:0;">
{{ $company->present()->name() }}</p> {{ $company->present()->name() }}</p>
<p style="font-size: 15px; color: #2e2e2e; font-family: 'roboto', Arial, Helvetica, sans-serif; font-weight: 400; margin-top: 5px;"> <p style="font-size: 15px; color: #2e2e2e; font-family: 'roboto', Arial, Helvetica, sans-serif; font-weight: 400; margin-top: 5px;">

View File

@ -53,6 +53,71 @@ class ClientApiTest extends TestCase
Model::reguard(); Model::reguard();
} }
public function testClientStatement()
{
$response = null;
$data = [
'client_id' => $this->client->hashed_id,
'start_date' => '2000-01-01',
'end_date' => '2023-01-01',
'show_aging_table' => true,
'show_payments_table' => true,
'status' => 'paid',
];
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/client_statement', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$this->assertTrue($response->headers->get('content-type') == 'application/pdf');
$response->assertStatus(200);
}
public function testClientStatementEmail()
{
$response = null;
$data = [
'client_id' => $this->client->hashed_id,
'start_date' => '2000-01-01',
'end_date' => '2023-01-01',
'show_aging_table' => true,
'show_payments_table' => true,
'status' => 'paid',
];
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/client_statement?send_email=true', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$response->assertJson([
'message' => ctrans('texts.email_queued'),
]);
$response->assertStatus(200);
}
public function testCsvImportRepositoryPersistance() public function testCsvImportRepositoryPersistance()
{ {
Client::unguard(); Client::unguard();

View File

@ -0,0 +1,84 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Feature;
use App\Services\Email\EmailObject;
use App\Services\Email\EmailService;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\MockAccountData;
use Tests\TestCase;
use Illuminate\Mail\Mailables\Address;
/**
* @test
* @covers App\Services\Email\EmailService
*/
class EmailTest extends TestCase
{
use MakesHash;
use GeneratesCounter;
use MockAccountData;
public EmailService $email_service;
public EmailObject $email_object;
protected function setUp() :void
{
parent::setUp();
if(!class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class))
$this->markTestSkipped('Skip test not needed in this environment');
$this->makeTestData();
$this->email_object = new EmailObject();
$this->email_object->to = [new Address("testing@gmail.com", "Cool Name")];
$this->email_object->attachments = [];
$this->email_object->settings = $this->client->getMergedSettings();
$this->email_object->company = $this->client->company;
$this->email_object->client = $this->client;
$this->email_object->email_template_subject = 'email_subject_statement';
$this->email_object->email_template_body = 'email_template_statement';
$this->email_object->variables = [
'$client' => $this->client->present()->name(),
'$start_date' => '2022-01-01',
'$end_date' => '2023-01-01',
];
$this->email_service = new EmailService($this->email_object, $this->company);
}
public function testPreFlightChecksHosted()
{
config(['ninja.environment' => 'hosted']);
$this->assertFalse($this->email_service->preFlightChecksFail());
}
public function testPreFlightChecksSelfHost()
{
config(['ninja.environment' => 'selfhost']);
$this->assertFalse($this->email_service->preFlightChecksFail());
}
}