mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-10 21:22:58 +01:00
Merge pull request #7328 from turbo124/v5-develop
Fixes for refunds / invoice restoration
This commit is contained in:
commit
5a89283cb9
@ -28,7 +28,7 @@ class ClientFactory
|
||||
$client->public_notes = '';
|
||||
$client->balance = 0;
|
||||
$client->paid_to_date = 0;
|
||||
$client->country_id = 840;
|
||||
$client->country_id = null;
|
||||
$client->is_deleted = 0;
|
||||
$client->client_hash = Str::random(40);
|
||||
$client->settings = ClientSettings::defaults();
|
||||
|
@ -45,7 +45,7 @@ class CompanyFactory
|
||||
|
||||
$company->enabled_modules = config('ninja.enabled_modules'); //32767;//8191; //4095
|
||||
$company->default_password_timeout = 1800000;
|
||||
|
||||
$company->markdown_email_enabled = true;
|
||||
|
||||
return $company;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ use App\Jobs\Account\CreateAccount;
|
||||
use App\Models\Account;
|
||||
use App\Models\CompanyUser;
|
||||
use App\Transformers\CompanyUserTransformer;
|
||||
use App\Utils\TruthSource;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
@ -150,7 +151,11 @@ class AccountController extends BaseController
|
||||
|
||||
$ct = CompanyUser::whereUserId(auth()->user()->id);
|
||||
|
||||
config(['ninja.company_id' => $ct->first()->company->id]);
|
||||
$truth = app()->make(TruthSource::class);
|
||||
$truth->setCompanyUser($ct->first());
|
||||
$truth->setUser(auth()->user());
|
||||
$truth->setCompany($ct->first()->company);
|
||||
|
||||
|
||||
return $this->listResponse($ct);
|
||||
}
|
||||
|
@ -35,7 +35,10 @@ class ContactRegisterController extends Controller
|
||||
public function showRegisterForm(string $company_key = '')
|
||||
{
|
||||
|
||||
$key = request()->session()->has('company_key') ? request()->session()->get('company_key') : $company_key;
|
||||
if(strlen($company_key) > 2)
|
||||
$key = $company_key;
|
||||
else
|
||||
$key = request()->session()->has('company_key') ? request()->session()->get('company_key') : $company_key;
|
||||
|
||||
$company = Company::where('company_key', $key)->firstOrFail();
|
||||
|
||||
@ -43,7 +46,7 @@ class ContactRegisterController extends Controller
|
||||
$t = app('translator');
|
||||
$t->replace(Ninja::transformTranslations($company->settings));
|
||||
|
||||
return render('auth.register', ['company' => $company, 'account' => $company->account]);
|
||||
return render('auth.register', ['register_company' => $company, 'account' => $company->account]);
|
||||
}
|
||||
|
||||
public function register(RegisterRequest $request)
|
||||
@ -60,6 +63,7 @@ class ContactRegisterController extends Controller
|
||||
|
||||
private function getClient(array $data)
|
||||
{
|
||||
|
||||
$client = ClientFactory::create($data['company']->id, $data['company']->owner()->id);
|
||||
|
||||
$client->fill($data);
|
||||
@ -67,14 +71,12 @@ class ContactRegisterController extends Controller
|
||||
$client->number = $this->getNextClientNumber($client);
|
||||
$client->save();
|
||||
|
||||
if(!$client->country_id && strlen($client->company->settings->country_id) > 1){
|
||||
if(!array_key_exists('country_id', $data) && strlen($client->company->settings->country_id) > 1){
|
||||
|
||||
$client->update(['country_id' => $client->company->settings->country_id]);
|
||||
|
||||
}
|
||||
|
||||
$this->getClientContact($data, $client);
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
|
@ -242,6 +242,8 @@ class LoginController extends BaseController
|
||||
|
||||
}
|
||||
|
||||
$truth->setCompanyToken(CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $user->account->default_company->id)->first());
|
||||
|
||||
/*On the hosted platform, only owners can login for free/pro accounts*/
|
||||
if(Ninja::isHosted() && !$cu->first()->is_owner && !$user->account->isEnterpriseClient())
|
||||
return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403);
|
||||
@ -388,13 +390,11 @@ class LoginController extends BaseController
|
||||
$cu = CompanyUser::query()
|
||||
->where('user_id', auth()->user()->id);
|
||||
|
||||
// $cu->first()->account->companies->each(function ($company) use($cu){
|
||||
|
||||
// if($company->tokens()->where('is_system', true)->count() == 0)
|
||||
// {
|
||||
// CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT'));
|
||||
// }
|
||||
// });
|
||||
$truth = app()->make(TruthSource::class);
|
||||
$truth->setCompanyUser($cu->first());
|
||||
$truth->setUser($existing_user);
|
||||
$truth->setCompany($existing_user->account->default_company);
|
||||
|
||||
|
||||
if($existing_user->company_users()->count() != $existing_user->tokens()->count())
|
||||
@ -412,6 +412,7 @@ class LoginController extends BaseController
|
||||
|
||||
}
|
||||
|
||||
$truth->setCompanyToken(CompanyToken::where('user_id', $existing_user->id)->where('company_id', $existing_user->account->default_company->id)->first());
|
||||
|
||||
|
||||
if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_user->account->isEnterpriseClient())
|
||||
@ -440,13 +441,11 @@ class LoginController extends BaseController
|
||||
$cu = CompanyUser::query()
|
||||
->where('user_id', auth()->user()->id);
|
||||
|
||||
// $cu->first()->account->companies->each(function ($company) use($cu){
|
||||
$truth = app()->make(TruthSource::class);
|
||||
$truth->setCompanyUser($cu->first());
|
||||
$truth->setUser($existing_login_user);
|
||||
$truth->setCompany($existing_login_user->account->default_company);
|
||||
|
||||
// if($company->tokens()->where('is_system', true)->count() == 0)
|
||||
// {
|
||||
// CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT'));
|
||||
// }
|
||||
// });
|
||||
|
||||
if($existing_login_user->company_users()->count() != $existing_login_user->tokens()->count())
|
||||
{
|
||||
@ -463,6 +462,8 @@ class LoginController extends BaseController
|
||||
|
||||
}
|
||||
|
||||
$truth->setCompanyToken(CompanyToken::where('user_id', $existing_login_user->id)->where('company_id', $existing_login_user->account->default_company->id)->first());
|
||||
|
||||
|
||||
|
||||
if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterpriseClient())
|
||||
@ -495,13 +496,10 @@ class LoginController extends BaseController
|
||||
$cu = CompanyUser::query()
|
||||
->where('user_id', auth()->user()->id);
|
||||
|
||||
// $cu->first()->account->companies->each(function ($company) use($cu){
|
||||
|
||||
// if($company->tokens()->where('is_system', true)->count() == 0)
|
||||
// {
|
||||
// CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT'));
|
||||
// }
|
||||
// });
|
||||
$truth = app()->make(TruthSource::class);
|
||||
$truth->setCompanyUser($cu->first());
|
||||
$truth->setUser($existing_login_user);
|
||||
$truth->setCompany($existing_login_user->account->default_company);
|
||||
|
||||
|
||||
if($existing_login_user->company_users()->count() != $existing_login_user->tokens()->count())
|
||||
@ -519,6 +517,7 @@ class LoginController extends BaseController
|
||||
|
||||
}
|
||||
|
||||
$truth->setCompanyToken(CompanyToken::where('user_id', $existing_login_user->id)->where('company_id', $existing_login_user->account->default_company->id)->first());
|
||||
|
||||
if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterpriseClient())
|
||||
return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403);
|
||||
@ -556,13 +555,11 @@ class LoginController extends BaseController
|
||||
|
||||
$cu = CompanyUser::whereUserId(auth()->user()->id);
|
||||
|
||||
// $cu->first()->account->companies->each(function ($company) use($cu){
|
||||
|
||||
// if($company->tokens()->where('is_system', true)->count() == 0)
|
||||
// {
|
||||
// CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT'));
|
||||
// }
|
||||
// });
|
||||
$truth = app()->make(TruthSource::class);
|
||||
$truth->setCompanyUser($cu->first());
|
||||
$truth->setUser(auth()->user());
|
||||
$truth->setCompany(auth()->user()->account->default_company);
|
||||
|
||||
if(auth()->user()->company_users()->count() != auth()->user()->tokens()->count())
|
||||
{
|
||||
@ -579,6 +576,7 @@ class LoginController extends BaseController
|
||||
|
||||
}
|
||||
|
||||
$truth->setCompanyToken(CompanyToken::where('user_id', auth()->user()->id)->where('company_id', auth()->user()->account->default_company->id)->first());
|
||||
|
||||
|
||||
if(Ninja::isHosted() && !$cu->first()->is_owner && !auth()->user()->account->isEnterpriseClient())
|
||||
|
@ -163,7 +163,7 @@ class NinjaPlanController extends Controller
|
||||
$recurring_invoice->save();
|
||||
$r = $recurring_invoice->calc()->getRecurringInvoice();
|
||||
|
||||
$recurring_invoice->service()->start()->save();
|
||||
$recurring_invoice->service()->applyNumber()->start()->save();
|
||||
|
||||
LightLogs::create(new TrialStarted())
|
||||
->increment()
|
||||
|
@ -53,10 +53,9 @@ class SetupController extends Controller
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
// not sure if we really need this.
|
||||
// if(File::exists(base_path('.env')))
|
||||
// abort(400, '.env file already exists, delete file to start Setup again.');
|
||||
|
||||
if(Ninja::isHosted())
|
||||
return redirect('/');
|
||||
|
||||
return view('setup.index', ['check' => $check]);
|
||||
}
|
||||
|
||||
|
@ -68,11 +68,9 @@ class ContactRegister
|
||||
|
||||
// For self-hosted platforms with multiple companies, resolving is done using company key
|
||||
// if it doesn't resolve using a domain.
|
||||
|
||||
if ($request->company_key && Ninja::isSelfHost()) {
|
||||
|
||||
if ($request->company_key && Ninja::isSelfHost() && $company = Company::where('company_key', $request->company_key)->first()) {
|
||||
|
||||
$company = Company::where('company_key', $request->company_key)->firstOrFail();
|
||||
|
||||
if(! (bool)$company->client_can_register)
|
||||
abort(400, 'Registration disabled');
|
||||
|
||||
|
@ -31,7 +31,7 @@ class TokenAuth
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if ($request->header('X-API-TOKEN') && ($company_token = CompanyToken::with(['user', 'company', 'cu'])->where('token', $request->header('X-API-TOKEN'))->first())) {
|
||||
if ($request->header('X-API-TOKEN') && ($company_token = CompanyToken::with(['user', 'company'])->where('token', $request->header('X-API-TOKEN'))->first())) {
|
||||
|
||||
$user = $company_token->user;
|
||||
|
||||
|
@ -18,7 +18,7 @@ class CreatePaymentMethodRequest extends FormRequest
|
||||
public function authorize(): bool
|
||||
{
|
||||
/** @var Client $client */
|
||||
$client = auth()->user()->client;
|
||||
$client = auth()->('guard')->user()->client;
|
||||
|
||||
$available_methods = [];
|
||||
|
||||
|
@ -29,7 +29,6 @@ class CreateProductRequest extends Request
|
||||
public function rules() : array
|
||||
{
|
||||
return [
|
||||
'product_key' => 'required',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ class CreateCompany
|
||||
$company->custom_fields = new \stdClass;
|
||||
$company->default_password_timeout = 1800000;
|
||||
$company->client_registration_fields = ClientRegistrationFields::generate();
|
||||
$company->markdown_email_enabled = true;
|
||||
|
||||
if(Ninja::isHosted())
|
||||
$company->subdomain = MultiDB::randomSubdomainGenerator();
|
||||
|
@ -210,15 +210,8 @@ class NinjaMailerJob implements ShouldQueue
|
||||
$user = $user->fresh();
|
||||
}
|
||||
|
||||
//17-01-2022 - ensure we have a token otherwise we fail gracefully to default sending engine
|
||||
// if(strlen($user->oauth_user_token) == 0){
|
||||
// $this->nmo->settings->email_sending_method = 'default';
|
||||
// return $this->setMailDriver();
|
||||
// }
|
||||
|
||||
$google->getClient()->setAccessToken(json_encode($user->oauth_user_token));
|
||||
|
||||
//need to slow down gmail requests otherwise we hit 429's
|
||||
sleep(rand(2,6));
|
||||
}
|
||||
catch(\Exception $e) {
|
||||
@ -227,6 +220,16 @@ class NinjaMailerJob implements ShouldQueue
|
||||
return $this->setMailDriver();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user doesn't have a valid token, notify them
|
||||
*/
|
||||
|
||||
if(!$user->oauth_user_token) {
|
||||
$this->company->account->gmailCredentialNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Now that our token is refreshed and valid we can boot the
|
||||
* mail driver at runtime and also set the token which will persist
|
||||
|
@ -42,8 +42,6 @@ class InvoiceArchivedActivity implements ShouldQueue
|
||||
public function handle($event)
|
||||
{
|
||||
MultiDB::setDb($event->company->db);
|
||||
|
||||
// $event->invoice->service()->deletePdf();
|
||||
|
||||
$fields = new stdClass;
|
||||
|
||||
|
64
app/Mail/Ninja/GmailTokenInvalid.php
Normal file
64
app/Mail/Ninja/GmailTokenInvalid.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?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 App\Mail\Ninja;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class GmailTokenInvalid extends Mailable
|
||||
{
|
||||
|
||||
public $company;
|
||||
|
||||
public $settings;
|
||||
|
||||
public $logo;
|
||||
|
||||
public $title;
|
||||
|
||||
public $body;
|
||||
|
||||
public $whitelabel;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($company)
|
||||
{
|
||||
$this->company = $company;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
App::setLocale($this->company->getLocale());
|
||||
|
||||
$this->settings = $this->company->settings;
|
||||
$this->logo = $this->company->present()->logo();
|
||||
$this->title = ctrans('texts.gmail_credentials_invalid_subject');
|
||||
$this->body = ctrans('texts.gmail_credentials_invalid_body');
|
||||
$this->whitelabel = $this->company->account->isPaid();
|
||||
$this->replyTo('contact@invoiceninja.com', 'Contact');
|
||||
|
||||
return $this->from(config('mail.from.address'), config('mail.from.name'))
|
||||
->subject(ctrans('texts.gmail_credentials_invalid_subject'))
|
||||
->view('email.admin.email_quota_exceeded');
|
||||
}
|
||||
}
|
@ -14,8 +14,10 @@ namespace App\Models;
|
||||
use App\Jobs\Mail\NinjaMailerJob;
|
||||
use App\Jobs\Mail\NinjaMailerObject;
|
||||
use App\Mail\Ninja\EmailQuotaExceeded;
|
||||
use App\Mail\Ninja\GmailTokenInvalid;
|
||||
use App\Models\Presenters\AccountPresenter;
|
||||
use App\Notifications\Ninja\EmailQuotaNotification;
|
||||
use App\Notifications\Ninja\GmailCredentialNotification;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Carbon\Carbon;
|
||||
@ -424,4 +426,43 @@ class Account extends BaseModel
|
||||
return false;
|
||||
}
|
||||
|
||||
public function gmailCredentialNotification() :bool
|
||||
{
|
||||
|
||||
if(is_null(Cache::get($this->key)))
|
||||
return false;
|
||||
|
||||
try {
|
||||
|
||||
if(is_null(Cache::get("gmail_credentials_notified:{$this->key}"))) {
|
||||
|
||||
App::forgetInstance('translator');
|
||||
$t = app('translator');
|
||||
$t->replace(Ninja::transformTranslations($this->companies()->first()->settings));
|
||||
|
||||
$nmo = new NinjaMailerObject;
|
||||
$nmo->mailable = new GmailTokenInvalid($this->companies()->first());
|
||||
$nmo->company = $this->companies()->first();
|
||||
$nmo->settings = $this->companies()->first()->settings;
|
||||
$nmo->to_user = $this->companies()->first()->owner();
|
||||
NinjaMailerJob::dispatch($nmo);
|
||||
|
||||
Cache::put("gmail_credentials_notified:{$this->key}", true, 60 * 24);
|
||||
|
||||
if(config('ninja.notification.slack'))
|
||||
$this->companies()->first()->notification(new GmailCredentialNotification($this))->ninja();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
catch(\Exception $e){
|
||||
\Sentry\captureMessage("I encountered an error with sending with gmail for account {$this->key}");
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -346,6 +346,8 @@ class CompanyGateway extends BaseModel
|
||||
if ($fees_and_limits->fee_amount) {
|
||||
$adjusted_fee += $fees_and_limits->fee_amount + $amount;
|
||||
}
|
||||
else
|
||||
$adjusted_fee = $amount;
|
||||
|
||||
if ($fees_and_limits->fee_percent) {
|
||||
|
||||
|
@ -59,6 +59,9 @@ class CompanyToken extends BaseModel
|
||||
|
||||
public function cu()
|
||||
{
|
||||
return $this->hasOneThrough(CompanyUser::class, Company::class, 'id', 'company_id', 'company_id', 'id');
|
||||
return $this->hasOne(CompanyUser::class, 'user_id', 'user_id')
|
||||
->where('company_id', $this->company_id)
|
||||
->where('user_id', $this->user_id);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -43,4 +43,5 @@ class Paymentable extends Pivot
|
||||
{
|
||||
return $this->belongsTo(Payment::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -66,11 +66,11 @@ class CompanyPresenter extends EntityPresenter
|
||||
);
|
||||
|
||||
if(strlen($settings->company_logo) >= 1 && (strpos($settings->company_logo, 'http') !== false))
|
||||
return "data:image/png;base64, ". base64_encode(file_get_contents($settings->company_logo, false, stream_context_create($context_options)));
|
||||
return "data:image/png;base64, ". base64_encode(@file_get_contents($settings->company_logo, false, stream_context_create($context_options)));
|
||||
else if(strlen($settings->company_logo) >= 1)
|
||||
return "data:image/png;base64, ". base64_encode(file_get_contents(url('') . $settings->company_logo, false, stream_context_create($context_options)));
|
||||
return "data:image/png;base64, ". base64_encode(@file_get_contents(url('') . $settings->company_logo, false, stream_context_create($context_options)));
|
||||
else
|
||||
return "data:image/png;base64, ". base64_encode(file_get_contents(asset('images/new_logo.png'), false, stream_context_create($context_options)));
|
||||
return "data:image/png;base64, ". base64_encode(@file_get_contents(asset('images/new_logo.png'), false, stream_context_create($context_options)));
|
||||
|
||||
}
|
||||
|
||||
|
@ -156,7 +156,6 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
return CompanyToken::with(['cu'])->where('token', request()->header('X-API-TOKEN'))->first();
|
||||
}
|
||||
|
||||
|
||||
return $this->tokens()->first();
|
||||
}
|
||||
|
||||
@ -371,9 +370,10 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
|
||||
return $this->isOwner() ||
|
||||
$this->isAdmin() ||
|
||||
(stripos($this->token()->cu->permissions, $all_permission) !== false) ||
|
||||
(stripos($this->token()->cu->permissions, $permission) !== false);
|
||||
(is_int(stripos($this->token()->cu->permissions, $all_permission))) ||
|
||||
(is_int(stripos($this->token()->cu->permissions, $permission)));
|
||||
|
||||
//23-03-2021 - stripos return an int if true and bool false, but 0 is also interpreted as false, so we simply use is_int() to verify state
|
||||
// return $this->isOwner() ||
|
||||
// $this->isAdmin() ||
|
||||
// (stripos($this->company_user->permissions, $all_permission) !== false) ||
|
||||
@ -404,9 +404,6 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
|
||||
if($this->token()->cu->slack_webhook_url)
|
||||
return $this->token()->cu->slack_webhook_url;
|
||||
// if ($this->company_user->slack_webhook_url) {
|
||||
// return $this->company_user->slack_webhook_url;
|
||||
// }
|
||||
}
|
||||
|
||||
public function routeNotificationForMail($notification)
|
||||
|
88
app/Notifications/Ninja/GmailCredentialNotification.php
Normal file
88
app/Notifications/Ninja/GmailCredentialNotification.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?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 App\Notifications\Ninja;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Messages\SlackMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class GmailCredentialNotification extends Notification
|
||||
{
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
||||
protected $account;
|
||||
|
||||
public function __construct($account)
|
||||
{
|
||||
$this->account = $account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['slack'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return MailMessage
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public function toSlack($notifiable)
|
||||
{
|
||||
|
||||
$content = "GMail credentials invalid for Account {$this->account->key} \n";
|
||||
|
||||
$owner = $this->account->companies()->first()->owner();
|
||||
|
||||
$content .= "Owner {$owner->present()->name() } | {$owner->email}";
|
||||
|
||||
return (new SlackMessage)
|
||||
->success()
|
||||
->from(ctrans('texts.notification_bot'))
|
||||
->image('https://app.invoiceninja.com/favicon.png')
|
||||
->content($content);
|
||||
}
|
||||
}
|
@ -322,8 +322,8 @@ class BaseDriver extends AbstractPaymentDriver
|
||||
|
||||
if (collect($invoice->line_items)->contains('type_id', '3')) {
|
||||
$invoice->service()->toggleFeesPaid()->save();
|
||||
$invoice->client->service()->updateBalance($fee_total)->save();
|
||||
$invoice->ledger()->updateInvoiceBalance($fee_total, "Gateway fee adjustment for invoice {$invoice->number}");
|
||||
// $invoice->client->service()->updateBalance($fee_total)->save();
|
||||
// $invoice->ledger()->updateInvoiceBalance($fee_total, "Gateway fee adjustment for invoice {$invoice->number}");
|
||||
}
|
||||
|
||||
$transaction = [
|
||||
|
@ -160,7 +160,6 @@ class CreditCard
|
||||
'TotalAmount' => $this->convertAmountForEway(),
|
||||
'CurrencyCode' => $this->eway_driver->client->currency()->code,
|
||||
'InvoiceNumber' => $invoice_numbers,
|
||||
'InvoiceReference' => $description,
|
||||
],
|
||||
'TransactionType' => \Eway\Rapid\Enum\TransactionType::PURCHASE,
|
||||
'SecuredCardData' => $request->input('securefieldcode'),
|
||||
@ -168,19 +167,17 @@ class CreditCard
|
||||
|
||||
$response = $this->eway_driver->init()->eway->createTransaction(\Eway\Rapid\Enum\ApiMethod::DIRECT, $transaction);
|
||||
|
||||
$this->logResponse($response);
|
||||
|
||||
$response_status = ErrorCode::getStatus($response->ResponseMessage);
|
||||
|
||||
if(!$response_status['success']){
|
||||
|
||||
$this->logResponse($response, false);
|
||||
|
||||
$this->eway_driver->sendFailureMail($response_status['message']);
|
||||
|
||||
throw new PaymentFailed($response_status['message'], 400);
|
||||
}
|
||||
|
||||
$this->logResponse($response, true);
|
||||
|
||||
$payment = $this->storePayment($response);
|
||||
|
||||
return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]);
|
||||
@ -252,13 +249,15 @@ class CreditCard
|
||||
'TotalAmount' => $this->convertAmountForEway($amount),
|
||||
'CurrencyCode' => $this->eway_driver->client->currency()->code,
|
||||
'InvoiceNumber' => $invoice_numbers,
|
||||
'InvoiceReference' => $description,
|
||||
],
|
||||
'TransactionType' => \Eway\Rapid\Enum\TransactionType::RECURRING,
|
||||
];
|
||||
|
||||
$response = $this->eway_driver->init()->eway->createTransaction(\Eway\Rapid\Enum\ApiMethod::DIRECT, $transaction);
|
||||
|
||||
nlog('eway');
|
||||
nlog($response);
|
||||
|
||||
$response_status = ErrorCode::getStatus($response->ResponseMessage);
|
||||
|
||||
if(!$response_status['success']){
|
||||
|
@ -58,12 +58,16 @@ class ClientRepository extends BaseRepository
|
||||
return $client;
|
||||
}
|
||||
|
||||
if(!$client->id && auth()->user() && auth()->user()->company() && (!array_key_exists('country_id', $data) || empty($data['country_id']))){
|
||||
$data['country_id'] = auth()->user()->company()->settings->country_id;
|
||||
$client->fill($data);
|
||||
|
||||
|
||||
if(auth()->user() && !$client->country_id){
|
||||
$client->country_id = auth()->user()->company()->settings->country_id;
|
||||
|
||||
}
|
||||
|
||||
$client->fill($data);
|
||||
$client->save();
|
||||
|
||||
|
||||
if (!isset($client->number) || empty($client->number) || strlen($client->number) == 0) {
|
||||
$client->number = $this->getNextClientNumber($client);
|
||||
|
@ -74,6 +74,8 @@ class AddGatewayFee extends AbstractService
|
||||
|
||||
private function processGatewayFee($gateway_fee)
|
||||
{
|
||||
$balance = $this->invoice->balance;
|
||||
|
||||
App::forgetInstance('translator');
|
||||
$t = app('translator');
|
||||
$t->replace(Ninja::transformTranslations($this->invoice->company->settings));
|
||||
@ -100,11 +102,30 @@ class AddGatewayFee extends AbstractService
|
||||
/**Refresh Invoice values*/
|
||||
$this->invoice = $this->invoice->calc()->getInvoice();
|
||||
|
||||
$new_balance = $this->invoice->balance;
|
||||
|
||||
if(floatval($new_balance) - floatval($balance) != 0)
|
||||
{
|
||||
$adjustment = $new_balance - $balance;
|
||||
|
||||
$this->invoice
|
||||
->client
|
||||
->service()
|
||||
->updateBalance($adjustment)
|
||||
->save();
|
||||
|
||||
$this->invoice
|
||||
->ledger()
|
||||
->updateInvoiceBalance($adjustment, 'Adjustment for removing gateway fee');
|
||||
}
|
||||
|
||||
return $this->invoice;
|
||||
}
|
||||
|
||||
private function processGatewayDiscount($gateway_fee)
|
||||
{
|
||||
$balance = $this->invoice->balance;
|
||||
|
||||
App::forgetInstance('translator');
|
||||
$t = app('translator');
|
||||
$t->replace(Ninja::transformTranslations($this->invoice->company->settings));
|
||||
@ -129,6 +150,25 @@ class AddGatewayFee extends AbstractService
|
||||
|
||||
$this->invoice = $this->invoice->calc()->getInvoice();
|
||||
|
||||
$new_balance = $this->invoice->balance;
|
||||
|
||||
|
||||
if(floatval($new_balance) - floatval($balance) != 0)
|
||||
{
|
||||
$adjustment = $new_balance - $balance;
|
||||
|
||||
$this->invoice
|
||||
->client
|
||||
->service()
|
||||
->updateBalance($adjustment * -1)
|
||||
->save();
|
||||
|
||||
$this->invoice
|
||||
->ledger()
|
||||
->updateInvoiceBalance($adjustment * -1, 'Adjustment for removing gateway fee');
|
||||
}
|
||||
|
||||
|
||||
return $this->invoice;
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,13 @@ class ApplyPaymentAmount extends AbstractService
|
||||
->deletePdf()
|
||||
->save();
|
||||
|
||||
$this->invoice
|
||||
->client
|
||||
->service()
|
||||
->updateBalance($payment->amount * -1)
|
||||
->updatePaidToDate($payment->amount)
|
||||
->save();
|
||||
|
||||
if ($this->invoice->client->getSetting('client_manual_payment_notification'))
|
||||
$payment->service()->sendEmail();
|
||||
|
||||
@ -92,13 +99,6 @@ class ApplyPaymentAmount extends AbstractService
|
||||
$payment->ledger()
|
||||
->updatePaymentBalance($payment->amount * -1);
|
||||
|
||||
$this->invoice
|
||||
->client
|
||||
->service()
|
||||
->updateBalance($payment->amount * -1)
|
||||
->updatePaidToDate($payment->amount)
|
||||
->save();
|
||||
|
||||
$this->invoice->service()->workFlow()->save();
|
||||
|
||||
event('eloquent.created: App\Models\Payment', $payment);
|
||||
|
@ -29,12 +29,16 @@ class GetInvoicePdf extends AbstractService
|
||||
|
||||
public function run()
|
||||
{
|
||||
|
||||
if (! $this->contact) {
|
||||
$this->contact = $this->invoice->client->primary_contact()->first() ?: $this->invoice->client->contacts()->first();
|
||||
}
|
||||
|
||||
$invitation = $this->invoice->invitations->where('client_contact_id', $this->contact->id)->first();
|
||||
|
||||
if(!$invitation)
|
||||
$invitation = $this->invoice->invitations->first();
|
||||
|
||||
$path = $this->invoice->client->invoice_filepath($invitation);
|
||||
|
||||
$file_path = $path.$this->invoice->numberFormatter().'.pdf';
|
||||
@ -48,8 +52,7 @@ class GetInvoicePdf extends AbstractService
|
||||
$file_path = CreateEntityPdf::dispatchNow($invitation);
|
||||
}
|
||||
|
||||
// return Storage::disk($disk)->path($file_path);
|
||||
//
|
||||
return $file_path;
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace App\Services\Invoice;
|
||||
|
||||
use App\Events\Invoice\InvoiceWasArchived;
|
||||
use App\Jobs\Entity\CreateEntityPdf;
|
||||
use App\Jobs\Invoice\InvoiceWorkflowSettings;
|
||||
use App\Jobs\Util\UnlinkFile;
|
||||
@ -361,6 +362,8 @@ class InvoiceService
|
||||
|
||||
public function removeUnpaidGatewayFees()
|
||||
{
|
||||
$balance = $this->invoice->balance;
|
||||
|
||||
//return early if type three does not exist.
|
||||
if(!collect($this->invoice->line_items)->contains('type_id', 3))
|
||||
return $this;
|
||||
@ -372,6 +375,25 @@ class InvoiceService
|
||||
|
||||
$this->invoice = $this->invoice->calc()->getInvoice();
|
||||
|
||||
/* 24-03-2022 */
|
||||
$new_balance = $this->invoice->balance;
|
||||
|
||||
if(floatval($balance) - floatval($new_balance) != 0)
|
||||
{
|
||||
$adjustment = $balance - $new_balance;
|
||||
|
||||
$this->invoice
|
||||
->client
|
||||
->service()
|
||||
->updateBalance($adjustment * -1)
|
||||
->save();
|
||||
|
||||
$this->invoice
|
||||
->ledger()
|
||||
->updateInvoiceBalance($adjustment * -1, 'Adjustment for removing gateway fee');
|
||||
}
|
||||
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -518,35 +540,16 @@ class InvoiceService
|
||||
if ($this->invoice->status_id == Invoice::STATUS_PAID && $this->invoice->client->getSetting('auto_archive_invoice')) {
|
||||
/* Throws: Payment amount xxx does not match invoice totals. */
|
||||
|
||||
$base_repository = new BaseRepository();
|
||||
$base_repository->archive($this->invoice);
|
||||
|
||||
}
|
||||
if ($this->invoice->trashed())
|
||||
return;
|
||||
|
||||
/*
|
||||
//if paid invoice is attached to a recurring invoice - check if we need to unpause the recurring invoice
|
||||
$this->invoice->delete();
|
||||
|
||||
event(new InvoiceWasArchived($this->invoice, $this->invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
||||
|
||||
if ($this->invoice->status_id == Invoice::STATUS_PAID &&
|
||||
$this->invoice->recurring_id &&
|
||||
$this->invoice->company->pause_recurring_until_paid &&
|
||||
($this->invoice->recurring_invoice->status_id != RecurringInvoice::STATUS_ACTIVE || $this->invoice->recurring_invoice->status_id != RecurringInvoice::STATUS_COMPLETED))
|
||||
{
|
||||
$recurring_invoice = $this->invoice->recurring_invoice;
|
||||
|
||||
// Check next_send_date if it is in the past - calculate
|
||||
$next_send_date = Carbon::parse($recurring_invoice->next_send_date)->startOfDay();
|
||||
|
||||
if(next_send_date->lt(now())){
|
||||
$recurring_invoice->next_send_date = $recurring_invoice->nextDateByFrequency(now()->format('Y-m-d'));
|
||||
$recurring_invoice->save();
|
||||
}
|
||||
|
||||
// Start the recurring invoice
|
||||
$recurring_invoice->service()
|
||||
->start();
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -263,6 +263,8 @@ class RefundPayment
|
||||
foreach ($this->refund_data['invoices'] as $refunded_invoice) {
|
||||
$invoice = Invoice::withTrashed()->find($refunded_invoice['invoice_id']);
|
||||
|
||||
$invoice->restore();
|
||||
|
||||
$invoice->service()->updateBalance($refunded_invoice['amount'])->save();
|
||||
$invoice->ledger()->updateInvoiceBalance($refunded_invoice['amount'], "Refund of payment # {$this->payment->number}")->save();
|
||||
$invoice->paid_to_date -= $refunded_invoice['amount'];
|
||||
@ -292,6 +294,9 @@ class RefundPayment
|
||||
|
||||
TransactionLog::dispatch(TransactionEvent::PAYMENT_REFUND, $transaction, $invoice->company->db);
|
||||
|
||||
if($invoice->is_deleted)
|
||||
$invoice->delete();
|
||||
|
||||
}
|
||||
|
||||
$client = $this->payment->client->fresh();
|
||||
|
@ -71,6 +71,9 @@ class UpdateInvoicePayment
|
||||
->updatePaidToDate($paid_amount)
|
||||
->updateStatus()
|
||||
->touchPdf()
|
||||
->save();
|
||||
|
||||
$invoice->service()
|
||||
->workFlow()
|
||||
->save();
|
||||
|
||||
|
@ -428,16 +428,33 @@ class Design extends BaseDesign
|
||||
|
||||
$tbody = [];
|
||||
|
||||
foreach ($this->payments as $payment) {
|
||||
foreach ($payment->invoices as $invoice) {
|
||||
// foreach ($this->payments as $payment) {
|
||||
// foreach ($payment->invoices as $invoice) {
|
||||
// $element = ['element' => 'tr', 'elements' => []];
|
||||
|
||||
// $element['elements'][] = ['element' => 'td', 'content' => $invoice->number];
|
||||
// $element['elements'][] = ['element' => 'td', 'content' => $this->translateDate($payment->date, $this->client->date_format(), $this->client->locale()) ?: ' '];
|
||||
// $element['elements'][] = ['element' => 'td', 'content' => $payment->type ? $payment->type->name : ctrans('texts.manual_entry')];
|
||||
// $element['elements'][] = ['element' => 'td', 'content' => Number::formatMoney($payment->amount, $this->client) ?: ' '];
|
||||
|
||||
// $tbody[] = $element;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
//24-03-2022 show payments per invoice
|
||||
foreach ($this->invoices as $invoice) {
|
||||
foreach ($invoice->payments as $payment) {
|
||||
|
||||
$element = ['element' => 'tr', 'elements' => []];
|
||||
|
||||
$element['elements'][] = ['element' => 'td', 'content' => $invoice->number];
|
||||
$element['elements'][] = ['element' => 'td', 'content' => $this->translateDate($payment->date, $this->client->date_format(), $this->client->locale()) ?: ' '];
|
||||
$element['elements'][] = ['element' => 'td', 'content' => $payment->type ? $payment->type->name : ctrans('texts.manual_entry')];
|
||||
$element['elements'][] = ['element' => 'td', 'content' => Number::formatMoney($payment->amount, $this->client) ?: ' '];
|
||||
$element['elements'][] = ['element' => 'td', 'content' => Number::formatMoney($payment->pivot->amount, $this->client) ?: ' '];
|
||||
|
||||
$tbody[] = $element;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -646,7 +663,7 @@ class Design extends BaseDesign
|
||||
return [
|
||||
['element' => 'div', 'properties' => ['style' => 'display: flex; flex-direction: column;'], 'elements' => [
|
||||
['element' => 'div', 'properties' => ['style' => 'margin-top: 1.5rem; display: block; align-items: flex-start; page-break-inside: avoid; visible !important;'], 'elements' => [
|
||||
['element' => 'img', 'properties' => ['src' => '$invoiceninja.whitelabel', 'style' => 'overflow: visible !important; display: block; page-break-inside: avoid; height: 2.5rem;', 'hidden' => $this->entity->user->account->isPaid() ? 'true' : 'false', 'id' => 'invoiceninja-whitelabel-logo']],
|
||||
['element' => 'img', 'properties' => ['src' => '$invoiceninja.whitelabel', 'style' => 'height: 2.5rem;', 'hidden' => $this->entity->user->account->isPaid() ? 'true' : 'false', 'id' => 'invoiceninja-whitelabel-logo']],
|
||||
]],
|
||||
]],
|
||||
];
|
||||
@ -658,21 +675,6 @@ class Design extends BaseDesign
|
||||
|
||||
$variables = $this->context['pdf_variables']['total_columns'];
|
||||
|
||||
/* 'labels' is a protected value - if the user enters labels it attempts to replace this string again - we need to set labels are a protected text label and remove it from the string */
|
||||
// $elements = [
|
||||
// ['element' => 'div', 'properties' => ['style' => 'display: flex; flex-direction: column;'], 'elements' => [
|
||||
// ['element' => 'p', 'content' => strtr(str_replace("labels", "", $_variables['values']['$entity.public_notes']), $_variables), 'properties' => ['data-ref' => 'total_table-public_notes', 'style' => 'text-align: left;']],
|
||||
// ['element' => 'p', 'content' => '', 'properties' => ['style' => 'text-align: left; display: flex; flex-direction: column;'], 'elements' => [
|
||||
// ['element' => 'span', 'content' => '$entity.terms_label: ', 'properties' => ['hidden' => $this->entityVariableCheck('$entity.terms'), 'data-ref' => 'total_table-terms-label', 'style' => 'font-weight: bold; text-align: left; margin-top: 1rem;']],
|
||||
// ['element' => 'span', 'content' => strtr(str_replace("labels", "", $_variables['values']['$entity.terms']), $_variables['labels']), 'properties' => ['data-ref' => 'total_table-terms', 'style' => 'text-align: left;']],
|
||||
// ]],
|
||||
// ['element' => 'img', 'properties' => ['style' => 'max-width: 50%; height: auto;', 'src' => '$contact.signature', 'id' => 'contact-signature']],
|
||||
// ['element' => 'div', 'properties' => ['style' => 'margin-top: 1.5rem; display: block; align-items: flex-start; page-break-inside: avoid; visible !important;'], 'elements' => [
|
||||
// ['element' => 'img', 'properties' => ['src' => '$invoiceninja.whitelabel', 'style' => 'overflow: visible !important; display: block; page-break-inside: avoid; height: 2.5rem;', 'hidden' => $this->entity->user->account->isPaid() ? 'true' : 'false', 'id' => 'invoiceninja-whitelabel-logo']],
|
||||
// ]],
|
||||
// ]],
|
||||
// ['element' => 'div', 'properties' => ['class' => 'totals-table-right-side', 'dir' => '$dir'], 'elements' => []],
|
||||
// ];
|
||||
|
||||
$elements = [
|
||||
['element' => 'div', 'properties' => ['style' => 'display: flex; flex-direction: column;'], 'elements' => [
|
||||
|
@ -17,11 +17,13 @@ use App\Factory\InvoiceInvitationFactory;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Quote;
|
||||
use App\Repositories\InvoiceRepository;
|
||||
use App\Utils\Traits\GeneratesConvertedQuoteCounter;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
|
||||
class ConvertQuote
|
||||
{
|
||||
use MakesHash;
|
||||
use GeneratesConvertedQuoteCounter;
|
||||
|
||||
private $client;
|
||||
|
||||
@ -49,6 +51,19 @@ class ConvertQuote
|
||||
$invoice_array = $invoice->toArray();
|
||||
$invoice_array['invitations'] = $invites;
|
||||
|
||||
//try and convert the invoice number to a quote number here.
|
||||
if($this->client->getSetting('shared_invoice_quote_counter'))
|
||||
{
|
||||
|
||||
$converted_number = $this->harvestQuoteCounter($quote, $invoice, $this->client);
|
||||
|
||||
if($converted_number)
|
||||
{
|
||||
$invoice_array['number'] = $converted_number;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$invoice = $this->invoice_repo->save($invoice_array, $invoice);
|
||||
|
||||
$invoice->fresh();
|
||||
|
@ -212,7 +212,8 @@ trait ClientGroupSettingsSaver
|
||||
case 'real':
|
||||
case 'float':
|
||||
case 'double':
|
||||
return is_float($value) || is_numeric(strval($value));
|
||||
return !is_string($value) && (is_float($value) || is_numeric(strval($value)));
|
||||
//return is_float($value) || is_numeric(strval($value));
|
||||
case 'string':
|
||||
return ( is_string( $value ) && method_exists($value, '__toString') ) || is_null($value) || is_string($value);
|
||||
case 'bool':
|
||||
|
@ -59,7 +59,8 @@ trait CompanyGatewayFeesAndLimitsSaver
|
||||
case 'real':
|
||||
case 'float':
|
||||
case 'double':
|
||||
return is_float($value) || is_numeric(strval($value));
|
||||
return !is_string($value) && (is_float($value) || is_numeric(strval($value)));
|
||||
// return is_float($value) || is_numeric(strval($value));
|
||||
case 'string':
|
||||
return ( is_string( $value ) && method_exists($value, '__toString') ) || is_null($value) || is_string($value);
|
||||
case 'bool':
|
||||
|
@ -193,6 +193,11 @@ trait CompanySettingsSaver
|
||||
settype($settings->{$key}, 'object');
|
||||
}
|
||||
|
||||
//try casting floats here
|
||||
if($value == 'float' && property_exists($settings, $key)){
|
||||
$settings->{$key} = floatval($settings->{$key});
|
||||
}
|
||||
|
||||
/* Handles unset settings or blank strings */
|
||||
if (! property_exists($settings, $key) || is_null($settings->{$key}) || ! isset($settings->{$key}) || $settings->{$key} == '') {
|
||||
continue;
|
||||
@ -229,7 +234,8 @@ trait CompanySettingsSaver
|
||||
case 'real':
|
||||
case 'float':
|
||||
case 'double':
|
||||
return is_float($value) || is_numeric(strval($value));
|
||||
return !is_string($value) && (is_float($value) || is_numeric(strval($value)));
|
||||
// return is_float($value) || is_numeric(strval($value));
|
||||
case 'string':
|
||||
return (is_string($value) && method_exists($value, '__toString')) || is_null($value) || is_string($value);
|
||||
case 'bool':
|
||||
|
758
app/Utils/Traits/GeneratesConvertedQuoteCounter.php
Normal file
758
app/Utils/Traits/GeneratesConvertedQuoteCounter.php
Normal file
@ -0,0 +1,758 @@
|
||||
<?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 App\Utils\Traits;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use App\Models\Client;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Expense;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Quote;
|
||||
use App\Models\RecurringExpense;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Models\RecurringQuote;
|
||||
use App\Models\Task;
|
||||
use App\Models\Timezone;
|
||||
use App\Models\Vendor;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class GeneratesConvertedQuoteCounter.
|
||||
*/
|
||||
trait GeneratesConvertedQuoteCounter
|
||||
{
|
||||
|
||||
private function harvestQuoteCounter($quote, $invoice, Client $client)
|
||||
{
|
||||
|
||||
$settings = $client->getMergedSettings();
|
||||
|
||||
$pattern = $settings->quote_number_pattern;
|
||||
|
||||
if(strlen($pattern) > 1 && (stripos($pattern, 'counter') === false))
|
||||
$pattern = $pattern.'{$counter}';
|
||||
|
||||
$number = $this->applyNumberPattern($quote, '_stubling_', $pattern);
|
||||
|
||||
$prefix_counter = str_replace('_stubling_', "", $number);
|
||||
$counter = str_replace($prefix_counter, "", $quote->number);
|
||||
|
||||
return $this->getNextEntityNumber($invoice, $client, intval($counter));
|
||||
}
|
||||
|
||||
private function getNextEntityNumber($invoice, Client $client, $counter)
|
||||
{
|
||||
|
||||
$settings = $client->getMergedSettings();
|
||||
|
||||
$pattern = $settings->invoice_number_pattern;
|
||||
|
||||
if(strlen($pattern) > 1 && (stripos($pattern, 'counter') === false)){
|
||||
$pattern = $pattern.'{$counter}';
|
||||
}
|
||||
|
||||
$padding = $client->getSetting('counter_padding');
|
||||
|
||||
$number = $this->padCounter($counter, $padding);
|
||||
|
||||
$number = $this->applyNumberPattern($invoice, $number, $pattern);
|
||||
|
||||
$check = Invoice::whereCompanyId($client->company_id)->whereNumber($number)->withTrashed()->exists();
|
||||
|
||||
if($check){
|
||||
return false;
|
||||
}
|
||||
|
||||
return $number;
|
||||
|
||||
}
|
||||
|
||||
private function getNumberPattern($entity, Client $client)
|
||||
{
|
||||
$pattern_string = '';
|
||||
|
||||
switch ($entity) {
|
||||
case Invoice::class:
|
||||
$pattern_string = 'invoice_number_pattern';
|
||||
break;
|
||||
case Quote::class:
|
||||
$pattern_string = 'quote_number_pattern';
|
||||
break;
|
||||
case RecurringInvoice::class:
|
||||
$pattern_string = 'recurring_invoice_number_pattern';
|
||||
break;
|
||||
case Payment::class:
|
||||
$pattern_string = 'payment_number_pattern';
|
||||
break;
|
||||
case Credit::class:
|
||||
$pattern_string = 'credit_number_pattern';
|
||||
break;
|
||||
case Project::class:
|
||||
$pattern_string = 'project_number_pattern';
|
||||
break;
|
||||
}
|
||||
|
||||
return $client->getSetting($pattern_string);
|
||||
}
|
||||
|
||||
private function getEntityCounter($entity, $client)
|
||||
{
|
||||
switch ($entity) {
|
||||
case Invoice::class:
|
||||
return 'invoice_number_counter';
|
||||
break;
|
||||
case Quote::class:
|
||||
|
||||
if ($this->hasSharedCounter($client, 'quote'))
|
||||
return 'invoice_number_counter';
|
||||
|
||||
return 'quote_number_counter';
|
||||
break;
|
||||
case RecurringInvoice::class:
|
||||
return 'recurring_invoice_number_counter';
|
||||
break;
|
||||
case RecurringQuote::class:
|
||||
return 'recurring_quote_number_counter';
|
||||
break;
|
||||
case RecurringExpense::class:
|
||||
return 'recurring_expense_number_counter';
|
||||
break;
|
||||
case Payment::class:
|
||||
return 'payment_number_counter';
|
||||
break;
|
||||
case Credit::class:
|
||||
if ($this->hasSharedCounter($client, 'credit'))
|
||||
return 'invoice_number_counter';
|
||||
|
||||
return 'credit_number_counter';
|
||||
break;
|
||||
case Project::class:
|
||||
return 'project_number_counter';
|
||||
break;
|
||||
|
||||
default:
|
||||
return 'default_number_counter';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next invoice number.
|
||||
*
|
||||
* @param Client $client The client
|
||||
*
|
||||
* @param Invoice|null $invoice
|
||||
* @return string The next invoice number.
|
||||
*/
|
||||
public function getNextInvoiceNumber(Client $client, ?Invoice $invoice, $is_recurring = false) :string
|
||||
{
|
||||
$entity_number = $this->getNextEntityNumber(Invoice::class, $client, $is_recurring);
|
||||
|
||||
return $this->replaceUserVars($invoice, $entity_number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next credit number.
|
||||
*
|
||||
* @param Client $client The client
|
||||
*
|
||||
* @return string The next credit number.
|
||||
*/
|
||||
public function getNextCreditNumber(Client $client, ?Credit $credit) :string
|
||||
{
|
||||
$entity_number = $this->getNextEntityNumber(Credit::class, $client);
|
||||
|
||||
return $this->replaceUserVars($credit, $entity_number);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next quote number.
|
||||
*
|
||||
* @param Client $client The client
|
||||
*
|
||||
* @return string The next credit number.
|
||||
*/
|
||||
public function getNextQuoteNumber(Client $client, ?Quote $quote)
|
||||
{
|
||||
$entity_number = $this->getNextEntityNumber(Quote::class, $client);
|
||||
|
||||
return $this->replaceUserVars($quote, $entity_number);
|
||||
|
||||
}
|
||||
|
||||
public function getNextRecurringInvoiceNumber(Client $client, $recurring_invoice)
|
||||
{
|
||||
$entity_number = $this->getNextEntityNumber(RecurringInvoice::class, $client);
|
||||
|
||||
return $this->replaceUserVars($recurring_invoice, $entity_number);
|
||||
|
||||
}
|
||||
|
||||
public function getNextRecurringQuoteNumber(Client $client, $recurring_quote)
|
||||
{
|
||||
$entity_number = $this->getNextEntityNumber(RecurringQuote::class, $client);
|
||||
|
||||
return $this->replaceUserVars($recurring_quote, $entity_number);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next Payment number.
|
||||
*
|
||||
* @param Client $client The client
|
||||
*
|
||||
* @return string The next payment number.
|
||||
*/
|
||||
public function getNextPaymentNumber(Client $client, ?Payment $payment) :string
|
||||
{
|
||||
$entity_number = $this->getNextEntityNumber(Payment::class, $client);
|
||||
|
||||
return $this->replaceUserVars($payment, $entity_number);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next client number.
|
||||
*
|
||||
* @param Client $client The client
|
||||
*
|
||||
* @return string The next client number.
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getNextClientNumber(Client $client) :string
|
||||
{
|
||||
//Reset counters if enabled
|
||||
$this->resetCounters($client);
|
||||
|
||||
$counter = $client->getSetting('client_number_counter');
|
||||
$setting_entity = $client->getSettingEntity('client_number_counter');
|
||||
|
||||
$client_number = $this->checkEntityNumber(Client::class, $client, $counter, $client->getSetting('counter_padding'), $client->getSetting('client_number_pattern'));
|
||||
|
||||
$this->incrementCounter($setting_entity, 'client_number_counter');
|
||||
|
||||
$entity_number = $client_number;
|
||||
|
||||
return $this->replaceUserVars($client, $entity_number);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the next client number.
|
||||
*
|
||||
* @param Vendor $vendor The vendor
|
||||
* @return string The next vendor number.
|
||||
*/
|
||||
public function getNextVendorNumber(Vendor $vendor) :string
|
||||
{
|
||||
$this->resetCompanyCounters($vendor->company);
|
||||
|
||||
$counter = $vendor->company->settings->vendor_number_counter;
|
||||
$setting_entity = $vendor->company->settings->vendor_number_counter;
|
||||
|
||||
$vendor_number = $this->checkEntityNumber(Vendor::class, $vendor, $counter, $vendor->company->settings->counter_padding, $vendor->company->settings->vendor_number_pattern);
|
||||
|
||||
$this->incrementCounter($vendor->company, 'vendor_number_counter');
|
||||
|
||||
$entity_number = $vendor_number;
|
||||
|
||||
return $this->replaceUserVars($vendor, $entity_number);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Project Number Generator.
|
||||
* @param Project $project
|
||||
* @return string The project number
|
||||
*/
|
||||
public function getNextProjectNumber(Project $project) :string
|
||||
{
|
||||
$entity_number = $this->getNextEntityNumber(Project::class, $project->client, false);
|
||||
|
||||
return $this->replaceUserVars($project, $entity_number);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the next task number.
|
||||
*
|
||||
* @param Task $task The task
|
||||
* @return string The next task number.
|
||||
*/
|
||||
public function getNextTaskNumber(Task $task) :string
|
||||
{
|
||||
$this->resetCompanyCounters($task->company);
|
||||
|
||||
$counter = $task->company->settings->task_number_counter;
|
||||
$setting_entity = $task->company->settings->task_number_counter;
|
||||
|
||||
$task_number = $this->checkEntityNumber(Task::class, $task, $counter, $task->company->settings->counter_padding, $task->company->settings->task_number_pattern);
|
||||
|
||||
$this->incrementCounter($task->company, 'task_number_counter');
|
||||
|
||||
$entity_number = $task_number;
|
||||
|
||||
return $this->replaceUserVars($task, $entity_number);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next expense number.
|
||||
*
|
||||
* @param Expense $expense The expense
|
||||
* @return string The next expense number.
|
||||
*/
|
||||
public function getNextExpenseNumber(Expense $expense) :string
|
||||
{
|
||||
$this->resetCompanyCounters($expense->company);
|
||||
|
||||
$counter = $expense->company->settings->expense_number_counter;
|
||||
$setting_entity = $expense->company->settings->expense_number_counter;
|
||||
|
||||
$expense_number = $this->checkEntityNumber(Expense::class, $expense, $counter, $expense->company->settings->counter_padding, $expense->company->settings->expense_number_pattern);
|
||||
|
||||
$this->incrementCounter($expense->company, 'expense_number_counter');
|
||||
|
||||
$entity_number = $expense_number;
|
||||
|
||||
return $this->replaceUserVars($expense, $entity_number);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next expense number.
|
||||
*
|
||||
* @param RecurringExpense $expense The expense
|
||||
* @return string The next expense number.
|
||||
*/
|
||||
public function getNextRecurringExpenseNumber(RecurringExpense $expense) :string
|
||||
{
|
||||
$this->resetCompanyCounters($expense->company);
|
||||
|
||||
// - 18/09/21 need to set this property if it doesn't exist. //todo refactor this for other properties
|
||||
if(!property_exists($expense->company->settings, 'recurring_expense_number_counter')){
|
||||
$settings = $expense->company->settings;
|
||||
$settings->recurring_expense_number_counter = 1;
|
||||
$settings->recurring_expense_number_pattern = '';
|
||||
$expense->company->settings = $settings;
|
||||
$expense->company->save();
|
||||
}
|
||||
|
||||
$counter = $expense->company->settings->recurring_expense_number_counter;
|
||||
$setting_entity = $expense->company->settings->recurring_expense_number_counter;
|
||||
|
||||
$expense_number = $this->checkEntityNumber(RecurringExpense::class, $expense, $counter, $expense->company->settings->counter_padding, $expense->company->settings->recurring_expense_number_pattern);
|
||||
|
||||
$this->incrementCounter($expense->company, 'recurring_expense_number_counter');
|
||||
|
||||
$entity_number = $expense_number;
|
||||
|
||||
return $this->replaceUserVars($expense, $entity_number);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines if it has shared counter.
|
||||
*
|
||||
* @param Client $client The client
|
||||
*
|
||||
* @return bool True if has shared counter, False otherwise.
|
||||
*/
|
||||
public function hasSharedCounter(Client $client, string $type = 'quote') : bool
|
||||
{
|
||||
if($type == 'quote')
|
||||
return (bool) $client->getSetting('shared_invoice_quote_counter');
|
||||
|
||||
if($type == 'credit')
|
||||
return (bool) $client->getSetting('shared_invoice_credit_counter');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the number has not already been used.
|
||||
*
|
||||
* @param $class
|
||||
* @param Collection $entity The entity ie App\Models\Client, Invoice, Quote etc
|
||||
* @param int $counter The counter
|
||||
* @param int $padding The padding
|
||||
*
|
||||
* @param string $pattern
|
||||
* @param string $prefix
|
||||
* @return string The padded and prefixed entity number
|
||||
*/
|
||||
private function checkEntityNumber($class, $entity, $counter, $padding, $pattern, $prefix = '')
|
||||
{
|
||||
|
||||
$check = false;
|
||||
$check_counter = 1;
|
||||
|
||||
do {
|
||||
|
||||
$number = $this->padCounter($counter, $padding);
|
||||
|
||||
$number = $this->applyNumberPattern($entity, $number, $pattern);
|
||||
|
||||
$number = $this->prefixCounter($number, $prefix);
|
||||
|
||||
$check = $class::whereCompanyId($entity->company_id)->whereNumber($number)->withTrashed()->exists();
|
||||
|
||||
$counter++;
|
||||
$check_counter++;
|
||||
|
||||
if($check_counter > 100)
|
||||
return $number . "_" . Str::random(5);
|
||||
|
||||
} while ($check);
|
||||
|
||||
return $number;
|
||||
}
|
||||
|
||||
|
||||
/*Check if a number is available for use. */
|
||||
public function checkNumberAvailable($class, $entity, $number) :bool
|
||||
{
|
||||
|
||||
if ($entity = $class::whereCompanyId($entity->company_id)->whereNumber($number)->withTrashed()->exists())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves counters at both the company and client level.
|
||||
*
|
||||
* @param $entity
|
||||
* @param string $counter_name The counter name
|
||||
*/
|
||||
private function incrementCounter($entity, string $counter_name) :void
|
||||
{
|
||||
$settings = $entity->settings;
|
||||
|
||||
if ($counter_name == 'invoice_number_counter' && ! property_exists($entity->settings, 'invoice_number_counter')) {
|
||||
$settings->invoice_number_counter = 0;
|
||||
}
|
||||
|
||||
if(!property_exists($settings, $counter_name))
|
||||
$settings->{$counter_name} = 1;
|
||||
|
||||
$settings->{$counter_name} = $settings->{$counter_name} + 1;
|
||||
|
||||
$entity->settings = $settings;
|
||||
|
||||
$entity->save();
|
||||
}
|
||||
|
||||
private function prefixCounter($counter, $prefix) : string
|
||||
{
|
||||
if (strlen($prefix) == 0) {
|
||||
return $counter;
|
||||
}
|
||||
|
||||
return $prefix.$counter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads a number with leading 000000's.
|
||||
*
|
||||
* @param int $counter The counter
|
||||
* @param int $padding The padding
|
||||
*
|
||||
* @return string the padded counter
|
||||
*/
|
||||
private function padCounter($counter, $padding) :string
|
||||
{
|
||||
return str_pad($counter, $padding, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are using counter reset,
|
||||
* check if we need to reset here.
|
||||
*
|
||||
* @param Client $client client entity
|
||||
* @return void
|
||||
*/
|
||||
private function resetCounters(Client $client)
|
||||
{
|
||||
$reset_counter_frequency = (int)$client->getSetting('reset_counter_frequency_id');
|
||||
|
||||
if($reset_counter_frequency == 0)
|
||||
return;
|
||||
|
||||
$timezone = Timezone::find($client->getSetting('timezone_id'));
|
||||
|
||||
$reset_date = Carbon::parse($client->getSetting('reset_counter_date'), $timezone->name);
|
||||
|
||||
if (! $reset_date->lte(now()) || ! $client->getSetting('reset_counter_date')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($reset_counter_frequency) {
|
||||
case RecurringInvoice::FREQUENCY_DAILY:
|
||||
$new_reset_date = $reset_date->addDay();
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_WEEKLY:
|
||||
$new_reset_date = $reset_date->addWeek();
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_TWO_WEEKS:
|
||||
$new_reset_date = $reset_date->addWeeks(2);
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_FOUR_WEEKS:
|
||||
$new_reset_date = $reset_date->addWeeks(4);
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_MONTHLY:
|
||||
$new_reset_date = $reset_date->addMonth();
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_TWO_MONTHS:
|
||||
$new_reset_date = $reset_date->addMonths(2);
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_THREE_MONTHS:
|
||||
$new_reset_date = $reset_date->addMonths(3);
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_FOUR_MONTHS:
|
||||
$new_reset_date = $reset_date->addMonths(4);
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_SIX_MONTHS:
|
||||
$new_reset_date = $reset_date->addMonths(6);
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_ANNUALLY:
|
||||
$new_reset_date = $reset_date->addYear();
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_TWO_YEARS:
|
||||
$new_reset_date = $reset_date->addYears(2);
|
||||
break;
|
||||
|
||||
default:
|
||||
$new_reset_date = $reset_date->addYear();
|
||||
break;
|
||||
}
|
||||
|
||||
$settings = $client->company->settings;
|
||||
$settings->reset_counter_date = $new_reset_date->format('Y-m-d');
|
||||
$settings->invoice_number_counter = 1;
|
||||
$settings->quote_number_counter = 1;
|
||||
$settings->credit_number_counter = 1;
|
||||
|
||||
$client->company->settings = $settings;
|
||||
$client->company->save();
|
||||
|
||||
}
|
||||
|
||||
private function resetCompanyCounters($company)
|
||||
{
|
||||
$timezone = Timezone::find($company->settings->timezone_id);
|
||||
|
||||
$reset_date = Carbon::parse($company->settings->reset_counter_date, $timezone->name);
|
||||
|
||||
if (! $reset_date->lte(now()) || ! $company->settings->reset_counter_date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($company->reset_counter_frequency_id) {
|
||||
case RecurringInvoice::FREQUENCY_DAILY:
|
||||
$reset_date->addDay();
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_WEEKLY:
|
||||
$reset_date->addWeek();
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_TWO_WEEKS:
|
||||
$reset_date->addWeeks(2);
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_FOUR_WEEKS:
|
||||
$reset_date->addWeeks(4);
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_MONTHLY:
|
||||
$reset_date->addMonth();
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_TWO_MONTHS:
|
||||
$reset_date->addMonths(2);
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_THREE_MONTHS:
|
||||
$reset_date->addMonths(3);
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_FOUR_MONTHS:
|
||||
$reset_date->addMonths(4);
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_SIX_MONTHS:
|
||||
$reset_date->addMonths(6);
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_ANNUALLY:
|
||||
$reset_date->addYear();
|
||||
break;
|
||||
case RecurringInvoice::FREQUENCY_TWO_YEARS:
|
||||
$reset_date->addYears(2);
|
||||
break;
|
||||
}
|
||||
|
||||
$settings = $company->settings;
|
||||
$settings->reset_counter_date = $reset_date->format('Y-m-d');
|
||||
$settings->invoice_number_counter = 1;
|
||||
$settings->quote_number_counter = 1;
|
||||
$settings->credit_number_counter = 1;
|
||||
$settings->vendor_number_counter = 1;
|
||||
$settings->ticket_number_counter = 1;
|
||||
$settings->payment_number_counter = 1;
|
||||
$settings->project_number_counter = 1;
|
||||
$settings->task_number_counter = 1;
|
||||
$settings->expense_number_counter = 1;
|
||||
$settings->recurring_expense_number_counter =1;
|
||||
|
||||
$company->settings = $settings;
|
||||
$company->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a entity number by pattern
|
||||
*
|
||||
* @param BaseModel $entity The entity object
|
||||
* @param string $counter The counter
|
||||
* @param null|string $pattern The pattern
|
||||
*
|
||||
* @return string The formatted number pattern
|
||||
*/
|
||||
private function applyNumberPattern($entity, string $counter, $pattern) :string
|
||||
{
|
||||
if (! $pattern) {
|
||||
return $counter;
|
||||
}
|
||||
|
||||
$search = ['{$year}'];
|
||||
$replace = [date('Y')];
|
||||
|
||||
$search[] = '{$counter}';
|
||||
$replace[] = $counter;
|
||||
|
||||
$search[] = '{$client_counter}';
|
||||
$replace[] = $counter;
|
||||
|
||||
$search[] = '{$clientCounter}';
|
||||
$replace[] = $counter;
|
||||
|
||||
$search[] = '{$group_counter}';
|
||||
$replace[] = $counter;
|
||||
|
||||
$search[] = '{$year}';
|
||||
$replace[] = date('Y');
|
||||
|
||||
if (strstr($pattern, '{$user_id}') || strstr($pattern, '{$userId}')) {
|
||||
$user_id = $entity->user_id ? $entity->user_id : 0;
|
||||
$search[] = '{$user_id}';
|
||||
$replace[] = str_pad(($user_id), 2, '0', STR_PAD_LEFT);
|
||||
$search[] = '{$userId}';
|
||||
$replace[] = str_pad(($user_id), 2, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
$matches = false;
|
||||
preg_match('/{\$date:(.*?)}/', $pattern, $matches);
|
||||
if (count($matches) > 1) {
|
||||
$format = $matches[1];
|
||||
$search[] = $matches[0];
|
||||
|
||||
/* The following adjusts for the company timezone - may bork tests depending on the time of day the tests are run!!!!!!*/
|
||||
$date = Carbon::now($entity->company->timezone()->name)->format($format);
|
||||
$replace[] = str_replace($format, $date, $matches[1]);
|
||||
}
|
||||
|
||||
if ($entity instanceof Vendor) {
|
||||
$search[] = '{$vendor_id_number}';
|
||||
$replace[] = $entity->id_number;
|
||||
}
|
||||
|
||||
if ($entity instanceof Expense) {
|
||||
if ($entity->vendor) {
|
||||
$search[] = '{$vendor_id_number}';
|
||||
$replace[] = $entity->vendor->id_number;
|
||||
|
||||
$search[] = '{$vendor_number}';
|
||||
$replace[] = $entity->vendor->number;
|
||||
|
||||
$search[] = '{$vendor_custom1}';
|
||||
$replace[] = $entity->vendor->custom_value1;
|
||||
|
||||
$search[] = '{$vendor_custom2}';
|
||||
$replace[] = $entity->vendor->custom_value2;
|
||||
|
||||
$search[] = '{$vendor_custom3}';
|
||||
$replace[] = $entity->vendor->custom_value3;
|
||||
|
||||
$search[] = '{$vendor_custom4}';
|
||||
$replace[] = $entity->vendor->custom_value4;
|
||||
}
|
||||
|
||||
$search[] = '{$expense_id_number}';
|
||||
$replace[] = $entity->id_number;
|
||||
}
|
||||
|
||||
if ($entity->client || ($entity instanceof Client)) {
|
||||
$client = $entity->client ?: $entity;
|
||||
|
||||
$search[] = '{$client_custom1}';
|
||||
$replace[] = $client->custom_value1;
|
||||
|
||||
$search[] = '{$clientCustom1}';
|
||||
$replace[] = $client->custom_value1;
|
||||
|
||||
$search[] = '{$client_custom2}';
|
||||
$replace[] = $client->custom_value2;
|
||||
|
||||
$search[] = '{$clientCustom2}';
|
||||
$replace[] = $client->custom_value2;
|
||||
|
||||
$search[] = '{$client_custom3}';
|
||||
$replace[] = $client->custom_value3;
|
||||
|
||||
$search[] = '{$client_custom4}';
|
||||
$replace[] = $client->custom_value4;
|
||||
|
||||
$search[] = '{$client_number}';
|
||||
$replace[] = $client->number;
|
||||
|
||||
$search[] = '{$client_id_number}';
|
||||
$replace[] = $client->id_number;
|
||||
|
||||
$search[] = '{$clientIdNumber}';
|
||||
$replace[] = $client->id_number;
|
||||
}
|
||||
|
||||
return str_replace($search, $replace, $pattern);
|
||||
}
|
||||
|
||||
private function replaceUserVars($entity, $pattern)
|
||||
{
|
||||
|
||||
if(!$entity)
|
||||
return $pattern;
|
||||
|
||||
$search = [];
|
||||
$replace = [];
|
||||
|
||||
$search[] = '{$user_custom1}';
|
||||
$replace[] = $entity->user->custom_value1;
|
||||
|
||||
$search[] = '{$user_custom2}';
|
||||
$replace[] = $entity->user->custom_value2;
|
||||
|
||||
$search[] = '{$user_custom3}';
|
||||
$replace[] = $entity->user->custom_value3;
|
||||
|
||||
$search[] = '{$user_custom4}';
|
||||
$replace[] = $entity->user->custom_value4;
|
||||
|
||||
return str_replace($search, $replace, $pattern);
|
||||
|
||||
}
|
||||
}
|
@ -293,26 +293,9 @@ trait GeneratesCounter
|
||||
*/
|
||||
public function getNextProjectNumber(Project $project) :string
|
||||
{
|
||||
// 08/12/2021 - allows projects to have client counters.
|
||||
|
||||
// $this->resetCompanyCounters($project->company);
|
||||
|
||||
// $counter = $project->company->settings->project_number_counter;
|
||||
// $setting_entity = $project->company->settings->project_number_counter;
|
||||
|
||||
// $project_number = $this->checkEntityNumber(Project::class, $project, $counter, $project->company->settings->counter_padding, $project->company->settings->project_number_pattern);
|
||||
|
||||
// $this->incrementCounter($project->company, 'project_number_counter');
|
||||
|
||||
// $entity_number = $project_number;
|
||||
|
||||
// return $this->replaceUserVars($project, $entity_number);
|
||||
|
||||
$entity_number = $this->getNextEntityNumber(Project::class, $project->client, false);
|
||||
|
||||
return $this->replaceUserVars($project, $entity_number);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -92,7 +92,8 @@ trait SettingsSaver
|
||||
case 'real':
|
||||
case 'float':
|
||||
case 'double':
|
||||
return is_float($value) || is_numeric(strval($value));
|
||||
return !is_string($value) && (is_float($value) || is_numeric(strval($value)));
|
||||
// return is_float($value) || is_numeric(strval($value));
|
||||
case 'string':
|
||||
return !is_int($value) || ( is_string( $value ) && method_exists($value, '__toString') ) || is_null($value) || is_string($value);
|
||||
case 'bool':
|
||||
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class MarkdownEmailEnabledWysiwygEditor extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('companies', function (Blueprint $table) {
|
||||
$table->boolean('markdown_email_enabled')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
@ -4570,6 +4570,8 @@ $LANG = array(
|
||||
'credits_backup_subject' => 'Your credits are ready for download',
|
||||
'document_download_subject' => 'Your documents are ready for download',
|
||||
'reminder_message' => 'Reminder for invoice :number for :balance',
|
||||
'gmail_credentials_invalid_subject' => 'Send with GMail invalid credentials',
|
||||
'gmail_credentials_invalid_body' => 'Your GMail credentials are not correct, please log into the administrator portal and navigate to Settings > User Details and disconnect and reconnect your GMail account. We will send you this notification daily until this issue is resolved',
|
||||
);
|
||||
|
||||
return $LANG;
|
||||
|
@ -5,21 +5,21 @@
|
||||
<div class="grid lg:grid-cols-12 py-8">
|
||||
<div class="col-span-12 lg:col-span-8 lg:col-start-3 xl:col-span-6 xl:col-start-4 px-6">
|
||||
<div class="flex justify-center">
|
||||
<img class="h-32 w-auto" src="{{ $company->present()->logo() }}" alt="{{ ctrans('texts.logo') }}">
|
||||
<img class="h-32 w-auto" src="{{ $register_company->present()->logo() }}" alt="{{ ctrans('texts.logo') }}">
|
||||
</div>
|
||||
<h1 class="text-center text-3xl mt-8">{{ ctrans('texts.register') }}</h1>
|
||||
<p class="block text-center text-gray-600">{{ ctrans('texts.register_label') }}</p>
|
||||
|
||||
<form action="{{ route('client.register', request()->route('company_key')) }}" method="POST" x-data="{ more: false }">
|
||||
@if($company)
|
||||
<input type="hidden" name="company_key" value="{{ $company->company_key }}">
|
||||
@if($register_company)
|
||||
<input type="hidden" name="company_key" value="{{ $register_company->company_key }}">
|
||||
@endif
|
||||
|
||||
@csrf
|
||||
|
||||
<div class="grid grid-cols-12 gap-4 mt-10">
|
||||
@if($company->client_registration_fields)
|
||||
@foreach($company->client_registration_fields as $field)
|
||||
@if($register_company->client_registration_fields)
|
||||
@foreach($register_company->client_registration_fields as $field)
|
||||
@if($field['required'])
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<section class="flex items-center">
|
||||
@ -108,15 +108,15 @@
|
||||
|
||||
<div class="flex justify-between items-center mt-8">
|
||||
<span class="inline-flex items-center" x-data="{ terms_of_service: false, privacy_policy: false }">
|
||||
@if(!empty($company->settings->client_portal_terms) || !empty($company->settings->client_portal_privacy_policy))
|
||||
@if(!empty($register_company->settings->client_portal_terms) || !empty($register_company->settings->client_portal_privacy_policy))
|
||||
<input type="checkbox" name="terms" class="form-checkbox mr-2 cursor-pointer" checked>
|
||||
<span class="text-sm text-gray-800">
|
||||
|
||||
{{ ctrans('texts.i_agree_to_the') }}
|
||||
@endif
|
||||
|
||||
@includeWhen(!empty($company->settings->client_portal_terms), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'terms_of_service', 'title' => ctrans('texts.terms_of_service'), 'content' => $company->settings->client_portal_terms])
|
||||
@includeWhen(!empty($company->settings->client_portal_privacy_policy), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'privacy_policy', 'title' => ctrans('texts.privacy_policy'), 'content' => $company->settings->client_portal_privacy_policy])
|
||||
@includeWhen(!empty($register_company->settings->client_portal_terms), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'terms_of_service', 'title' => ctrans('texts.terms_of_service'), 'content' => $register_company->settings->client_portal_terms])
|
||||
@includeWhen(!empty($register_company->settings->client_portal_privacy_policy), 'portal.ninja2020.auth.includes.register.popup', ['property' => 'privacy_policy', 'title' => ctrans('texts.privacy_policy'), 'content' => $register_company->settings->client_portal_privacy_policy])
|
||||
|
||||
@error('terms')
|
||||
<p class="text-red-600">{{ $message }}</p>
|
||||
|
@ -74,7 +74,7 @@
|
||||
{{-- Feel free to push anything to header using @push('header') --}}
|
||||
@stack('head')
|
||||
|
||||
@if((bool) \App\Utils\Ninja::isSelfHost() && !empty($client->getSetting('portal_custom_head')))
|
||||
@if((isset($account) && $account->isPaid()) || ((bool) \App\Utils\Ninja::isSelfHost() && !empty($client->getSetting('portal_custom_head'))))
|
||||
<div class="py-1 text-sm text-center text-white bg-primary">
|
||||
{!! $client->getSetting('portal_custom_head') !!}
|
||||
</div>
|
||||
|
153
tests/Unit/GeneratesConvertedQuoteCounterTest.php
Normal file
153
tests/Unit/GeneratesConvertedQuoteCounterTest.php
Normal file
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://opensource.org/licenses/AAL
|
||||
*/
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\DataMapper\ClientSettings;
|
||||
use App\Factory\ClientFactory;
|
||||
use App\Factory\QuoteFactory;
|
||||
use App\Factory\VendorFactory;
|
||||
use App\Models\Account;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\Company;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Quote;
|
||||
use App\Models\RecurringInvoice;
|
||||
use App\Models\Timezone;
|
||||
use App\Models\User;
|
||||
use App\Utils\Traits\GeneratesConvertedQuoteCounter;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Tests\MockAccountData;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @covers App\Utils\Traits\GeneratesConvertedQuoteCounter
|
||||
*/
|
||||
class GeneratesConvertedQuoteCounterTest extends TestCase
|
||||
{
|
||||
use GeneratesConvertedQuoteCounter;
|
||||
use DatabaseTransactions;
|
||||
use MakesHash;
|
||||
|
||||
public function setUp() :void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Session::start();
|
||||
$this->faker = \Faker\Factory::create();
|
||||
Model::reguard();
|
||||
|
||||
}
|
||||
|
||||
public function testCounterExtraction()
|
||||
{
|
||||
|
||||
$this->account = Account::factory()->create([
|
||||
'hosted_client_count' => 1000,
|
||||
'hosted_company_count' => 1000
|
||||
]);
|
||||
|
||||
$this->account->num_users = 3;
|
||||
$this->account->save();
|
||||
|
||||
$user = User::whereEmail('user@example.com')->first();
|
||||
|
||||
if (! $user) {
|
||||
$user = User::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'confirmation_code' => $this->createDbHash(config('database.default')),
|
||||
'email' => 'user@example.com',
|
||||
]);
|
||||
}
|
||||
|
||||
$user_id = $user->id;
|
||||
|
||||
$this->company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
]);
|
||||
|
||||
$this->client = Client::factory()->create([
|
||||
'user_id' => $user_id,
|
||||
'company_id' => $this->company->id,
|
||||
]);
|
||||
|
||||
$contact = ClientContact::factory()->create([
|
||||
'user_id' => $user_id,
|
||||
'client_id' => $this->client->id,
|
||||
'company_id' => $this->company->id,
|
||||
'is_primary' => 1,
|
||||
'send_email' => true,
|
||||
]);
|
||||
|
||||
$settings = $this->client->getMergedSettings();
|
||||
$settings->invoice_number_counter = 1;
|
||||
$settings->invoice_number_pattern = '{$year}-I{$counter}';
|
||||
$settings->quote_number_pattern = '{$year}-Q{$counter}';
|
||||
$settings->shared_invoice_quote_counter = 1;
|
||||
$this->company->settings = $settings;
|
||||
|
||||
$this->company->save();
|
||||
|
||||
$this->client->settings = $settings;
|
||||
$this->client->save();
|
||||
|
||||
$quote = Quote::factory()->create([
|
||||
'user_id' => $this->client->user_id,
|
||||
'company_id' => $this->client->company_id,
|
||||
'client_id' => $this->client->id
|
||||
]);
|
||||
|
||||
$quote = $quote->service()->markSent()->convert()->save();
|
||||
|
||||
$invoice = Invoice::find($quote->invoice_id);
|
||||
|
||||
$this->assertNotNull($invoice);
|
||||
|
||||
$this->assertEquals('2022-Q0001', $quote->number);
|
||||
$this->assertEquals('2022-I0001', $invoice->number);
|
||||
|
||||
|
||||
$settings = $this->client->getMergedSettings();
|
||||
$settings->invoice_number_counter = 100;
|
||||
$settings->invoice_number_pattern = 'I{$counter}';
|
||||
$settings->quote_number_pattern = 'Q{$counter}';
|
||||
$settings->shared_invoice_quote_counter = 1;
|
||||
$this->company->settings = $settings;
|
||||
|
||||
$this->company->save();
|
||||
|
||||
$this->client->settings = $settings;
|
||||
$this->client->save();
|
||||
|
||||
$quote = Quote::factory()->create([
|
||||
'user_id' => $this->client->user_id,
|
||||
'company_id' => $this->client->company_id,
|
||||
'client_id' => $this->client->id
|
||||
]);
|
||||
|
||||
$quote = $quote->service()->markSent()->convert()->save();
|
||||
|
||||
$invoice = Invoice::find($quote->invoice_id);
|
||||
|
||||
$this->assertNotNull($invoice);
|
||||
|
||||
$this->assertEquals('Q0100', $quote->number);
|
||||
$this->assertEquals('I0100', $invoice->number);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user