Merge pull request #7330 from turbo124/v5-stable

v5.3.76
This commit is contained in:
David Bomba 2022-03-26 22:27:33 +11:00 committed by GitHub
commit a78a167665
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 101169 additions and 99864 deletions

View File

@ -1 +1 @@
5.3.75
5.3.76

View File

@ -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();

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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())

View File

@ -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()

View File

@ -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]);
}

View File

@ -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');

View File

@ -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;

View File

@ -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 = [];

View File

@ -29,7 +29,6 @@ class CreateProductRequest extends Request
public function rules() : array
{
return [
'product_key' => 'required',
];
}
}

View File

@ -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();

View File

@ -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

View File

@ -42,8 +42,6 @@ class InvoiceArchivedActivity implements ShouldQueue
public function handle($event)
{
MultiDB::setDb($event->company->db);
// $event->invoice->service()->deletePdf();
$fields = new stdClass;

View 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');
}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -43,4 +43,5 @@ class Paymentable extends Pivot
{
return $this->belongsTo(Payment::class);
}
}

View File

@ -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)));
}

View File

@ -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)

View 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);
}
}

View File

@ -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 = [

View File

@ -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']){

View File

@ -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);

View File

@ -28,8 +28,6 @@ class ClientService
public function updateBalance(float $amount)
{
// $this->client->balance += $amount;
$this->client->increment('balance', $amount);
return $this;
@ -37,8 +35,6 @@ class ClientService
public function updatePaidToDate(float $amount)
{
// $this->client->paid_to_date += $amount;
$this->client->increment('paid_to_date', $amount);
return $this;
@ -46,8 +42,6 @@ class ClientService
public function adjustCreditBalance(float $amount)
{
// $this->client->credit_balance += $amount;
$this->client->increment('credit_balance', $amount);
return $this;

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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;
@ -145,11 +146,8 @@ class InvoiceService
return $this;
}
// $this->invoice->balance += $balance_adjustment;
$this->invoice->balance += $balance_adjustment;
$this->invoice->increment('balance', $balance_adjustment);
if (round($this->invoice->balance,2) == 0 && !$is_draft) {
$this->invoice->status_id = Invoice::STATUS_PAID;
}
@ -163,10 +161,7 @@ class InvoiceService
public function updatePaidToDate($adjustment)
{
// $this->invoice->paid_to_date += $adjustment;
$this->invoice->increment('paid_to_date', $adjustment);
$this->invoice->paid_to_date += $adjustment;
return $this;
}
@ -361,6 +356,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 +369,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 +534,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;
}

View File

@ -263,9 +263,15 @@ class RefundPayment
foreach ($this->refund_data['invoices'] as $refunded_invoice) {
$invoice = Invoice::withTrashed()->find($refunded_invoice['invoice_id']);
$invoice->service()->updateBalance($refunded_invoice['amount'])->save();
if($invoice->trashed())
$invoice->restore();
$invoice->service()
->updateBalance($refunded_invoice['amount'])
->updatePaidToDate($refunded_invoice['amount'] * -1)
->save();
$invoice->ledger()->updateInvoiceBalance($refunded_invoice['amount'], "Refund of payment # {$this->payment->number}")->save();
$invoice->paid_to_date -= $refunded_invoice['amount'];
if ($invoice->amount == $invoice->balance) {
$invoice->service()->setStatus(Invoice::STATUS_SENT);
@ -276,7 +282,6 @@ class RefundPayment
$invoice->saveQuietly();
$client = $invoice->client;
$adjustment_amount += $refunded_invoice['amount'];
$client->balance += $refunded_invoice['amount'];
$client->save();
@ -292,11 +297,17 @@ class RefundPayment
TransactionLog::dispatch(TransactionEvent::PAYMENT_REFUND, $transaction, $invoice->company->db);
if($invoice->is_deleted)
$invoice->delete();
}
$client = $this->payment->client->fresh();
$client->service()->updatePaidToDate(-1 * $refunded_invoice['amount'])->save();
if($client->trashed())
$client->restore();
$client->service()->updatePaidToDate(-1 * $refunded_invoice['amount'])->save();
$transaction = [
'invoice' => [],
@ -313,6 +324,10 @@ class RefundPayment
//if we are refunding and no payments have been tagged, then we need to decrement the client->paid_to_date by the total refund amount.
$client = $this->payment->client->fresh();
if($client->trashed())
$client->restore();
$client->service()->updatePaidToDate(-1 * $this->total_refund)->save();
$transaction = [

View File

@ -43,12 +43,19 @@ class UpdateInvoicePayment
collect($paid_invoices)->each(function ($paid_invoice) use ($invoices) {
$client = $this->payment->client->fresh();
$client = $this->payment->client;
if($client->trashed())
$client->restore();
$invoice = $invoices->first(function ($inv) use ($paid_invoice) {
return $paid_invoice->invoice_id == $inv->hashed_id;
});
if($invoice->trashed())
$invoice->restore();
if ($invoice->id == $this->payment_hash->fee_invoice_id) {
$paid_amount = $paid_invoice->amount + $this->payment_hash->fee_total;
} else {
@ -71,6 +78,9 @@ class UpdateInvoicePayment
->updatePaidToDate($paid_amount)
->updateStatus()
->touchPdf()
->save();
$invoice->service()
->workFlow()
->save();
@ -113,7 +123,6 @@ class UpdateInvoicePayment
$invoices->each(function ($invoice) {
$invoice = $invoice->fresh();
event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
});

View File

@ -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()) ?: '&nbsp;'];
// $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) ?: '&nbsp;'];
// $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()) ?: '&nbsp;'];
$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) ?: '&nbsp;'];
$element['elements'][] = ['element' => 'td', 'content' => Number::formatMoney($payment->pivot->amount, $this->client) ?: '&nbsp;'];
$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' => [

View File

@ -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();

View File

@ -370,6 +370,8 @@ class HtmlEngine
$data['$client.currency'] = ['value' => $this->client->currency()->code, 'label' => ''];
$data['$client.lang_2'] = ['value' => optional($this->client->language())->locale, 'label' => ''];
$data['$client.balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')];
$data['$client_balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')];
$data['$paid_to_date'] = ['value' => Number::formatMoney($this->entity->paid_to_date, $this->client), 'label' => ctrans('texts.paid_to_date')];

View File

@ -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':

View File

@ -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':

View File

@ -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':

View 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);
}
}

View File

@ -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);
}

View File

@ -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':

View File

@ -148,4 +148,4 @@
},
"minimum-stability": "dev",
"prefer-stable": true
}
}

153
composer.lock generated
View File

@ -110,16 +110,16 @@
},
{
"name": "apimatic/unirest-php",
"version": "2.2.1",
"version": "2.2.2",
"source": {
"type": "git",
"url": "https://github.com/apimatic/unirest-php.git",
"reference": "847149c56d850081c07d5677e9647fa0c15e595f"
"reference": "a45c4c71a1ea3659b118042a67cc1b6486bcf03a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/apimatic/unirest-php/zipball/847149c56d850081c07d5677e9647fa0c15e595f",
"reference": "847149c56d850081c07d5677e9647fa0c15e595f",
"url": "https://api.github.com/repos/apimatic/unirest-php/zipball/a45c4c71a1ea3659b118042a67cc1b6486bcf03a",
"reference": "a45c4c71a1ea3659b118042a67cc1b6486bcf03a",
"shasum": ""
},
"require": {
@ -168,9 +168,9 @@
"support": {
"email": "opensource@apimatic.io",
"issues": "https://github.com/apimatic/unirest-php/issues",
"source": "https://github.com/apimatic/unirest-php/tree/2.2.1"
"source": "https://github.com/apimatic/unirest-php/tree/2.2.2"
},
"time": "2022-03-12T08:37:06+00:00"
"time": "2022-03-24T08:19:20+00:00"
},
{
"name": "asm/php-ansible",
@ -434,16 +434,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.215.0",
"version": "3.216.2",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "79c4ffdf93cdcc7be9196ae2e22f0d0323cb2557"
"reference": "9e09386c3a5b313eeb324490beff4eb843ed339d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/79c4ffdf93cdcc7be9196ae2e22f0d0323cb2557",
"reference": "79c4ffdf93cdcc7be9196ae2e22f0d0323cb2557",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/9e09386c3a5b313eeb324490beff4eb843ed339d",
"reference": "9e09386c3a5b313eeb324490beff4eb843ed339d",
"shasum": ""
},
"require": {
@ -519,9 +519,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.215.0"
"source": "https://github.com/aws/aws-sdk-php/tree/3.216.2"
},
"time": "2022-03-18T18:16:01+00:00"
"time": "2022-03-25T18:17:03+00:00"
},
{
"name": "bacon/bacon-qr-code",
@ -1300,16 +1300,16 @@
},
{
"name": "doctrine/dbal",
"version": "3.3.3",
"version": "3.3.4",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "82331b861727c15b1f457ef05a8729e508e7ead5"
"reference": "83f779beaea1893c0bece093ab2104c6d15a7f26"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/82331b861727c15b1f457ef05a8729e508e7ead5",
"reference": "82331b861727c15b1f457ef05a8729e508e7ead5",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/83f779beaea1893c0bece093ab2104c6d15a7f26",
"reference": "83f779beaea1893c0bece093ab2104c6d15a7f26",
"shasum": ""
},
"require": {
@ -1391,7 +1391,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/3.3.3"
"source": "https://github.com/doctrine/dbal/tree/3.3.4"
},
"funding": [
{
@ -1407,7 +1407,7 @@
"type": "tidelift"
}
],
"time": "2022-03-09T15:39:50+00:00"
"time": "2022-03-20T18:37:29+00:00"
},
{
"name": "doctrine/deprecations",
@ -2287,16 +2287,16 @@
},
{
"name": "google/apiclient-services",
"version": "v0.239.0",
"version": "v0.240.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client-services.git",
"reference": "ce8e34d618bdef9e824fd1728d505a468818712c"
"reference": "c6d39cda24d7ada02ecf8c464a1c8adb02607ba7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/ce8e34d618bdef9e824fd1728d505a468818712c",
"reference": "ce8e34d618bdef9e824fd1728d505a468818712c",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/c6d39cda24d7ada02ecf8c464a1c8adb02607ba7",
"reference": "c6d39cda24d7ada02ecf8c464a1c8adb02607ba7",
"shasum": ""
},
"require": {
@ -2325,29 +2325,29 @@
],
"support": {
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.239.0"
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.240.0"
},
"time": "2022-03-13T01:20:35+00:00"
"time": "2022-03-21T01:20:11+00:00"
},
{
"name": "google/auth",
"version": "v1.18.0",
"version": "v1.19.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-auth-library-php.git",
"reference": "21dd478e77b0634ed9e3a68613f74ed250ca9347"
"reference": "31e5d24d5fa0eaf6adc7e596292dc4732f4b60c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/21dd478e77b0634ed9e3a68613f74ed250ca9347",
"reference": "21dd478e77b0634ed9e3a68613f74ed250ca9347",
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/31e5d24d5fa0eaf6adc7e596292dc4732f4b60c5",
"reference": "31e5d24d5fa0eaf6adc7e596292dc4732f4b60c5",
"shasum": ""
},
"require": {
"firebase/php-jwt": "~2.0|~3.0|~4.0|~5.0",
"guzzlehttp/guzzle": "^5.3.1|^6.2.1|^7.0",
"firebase/php-jwt": "~5.0",
"guzzlehttp/guzzle": "^6.2.1|^7.0",
"guzzlehttp/psr7": "^1.7|^2.0",
"php": ">=5.4",
"php": ">=5.6",
"psr/cache": "^1.0|^2.0",
"psr/http-message": "^1.0"
},
@ -2355,8 +2355,10 @@
"guzzlehttp/promises": "0.1.1|^1.3",
"kelvinmo/simplejwt": "^0.2.5|^0.5.1",
"phpseclib/phpseclib": "^2.0.31",
"phpunit/phpunit": "^4.8.36|^5.7",
"sebastian/comparator": ">=1.2.3"
"phpspec/prophecy-phpunit": "^1.1",
"phpunit/phpunit": "^5.7||^8.5.13",
"sebastian/comparator": ">=1.2.3",
"squizlabs/php_codesniffer": "^3.5"
},
"suggest": {
"phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2."
@ -2379,11 +2381,11 @@
"oauth2"
],
"support": {
"docs": "https://googleapis.github.io/google-auth-library-php/master/",
"docs": "https://googleapis.github.io/google-auth-library-php/main/",
"issues": "https://github.com/googleapis/google-auth-library-php/issues",
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.18.0"
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.19.0"
},
"time": "2021-08-24T18:03:18+00:00"
"time": "2022-03-24T21:22:45+00:00"
},
{
"name": "graham-campbell/result-type",
@ -2507,16 +2509,16 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "7.4.1",
"version": "7.4.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "ee0a041b1760e6a53d2a39c8c34115adc2af2c79"
"reference": "ac1ec1cd9b5624694c3a40be801d94137afb12b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/ee0a041b1760e6a53d2a39c8c34115adc2af2c79",
"reference": "ee0a041b1760e6a53d2a39c8c34115adc2af2c79",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/ac1ec1cd9b5624694c3a40be801d94137afb12b4",
"reference": "ac1ec1cd9b5624694c3a40be801d94137afb12b4",
"shasum": ""
},
"require": {
@ -2611,7 +2613,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.4.1"
"source": "https://github.com/guzzle/guzzle/tree/7.4.2"
},
"funding": [
{
@ -2627,7 +2629,7 @@
"type": "tidelift"
}
],
"time": "2021-12-06T18:43:05+00:00"
"time": "2022-03-20T14:16:28+00:00"
},
{
"name": "guzzlehttp/promises",
@ -2715,16 +2717,16 @@
},
{
"name": "guzzlehttp/psr7",
"version": "2.1.0",
"version": "2.2.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "089edd38f5b8abba6cb01567c2a8aaa47cec4c72"
"reference": "c94a94f120803a18554c1805ef2e539f8285f9a2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/089edd38f5b8abba6cb01567c2a8aaa47cec4c72",
"reference": "089edd38f5b8abba6cb01567c2a8aaa47cec4c72",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/c94a94f120803a18554c1805ef2e539f8285f9a2",
"reference": "c94a94f120803a18554c1805ef2e539f8285f9a2",
"shasum": ""
},
"require": {
@ -2748,7 +2750,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
"dev-master": "2.2-dev"
}
},
"autoload": {
@ -2810,7 +2812,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.1.0"
"source": "https://github.com/guzzle/psr7/tree/2.2.1"
},
"funding": [
{
@ -2826,7 +2828,7 @@
"type": "tidelift"
}
],
"time": "2021-10-06T17:43:30+00:00"
"time": "2022-03-20T21:55:58+00:00"
},
{
"name": "halaxa/json-machine",
@ -4616,16 +4618,16 @@
},
{
"name": "mollie/mollie-api-php",
"version": "v2.40.2",
"version": "v2.41.0",
"source": {
"type": "git",
"url": "https://github.com/mollie/mollie-api-php.git",
"reference": "ac3e079bbc86e95dc77d4f33965a62e9e6b95ed8"
"reference": "26010bd6999af4466c31c92733df87dc04d95772"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mollie/mollie-api-php/zipball/ac3e079bbc86e95dc77d4f33965a62e9e6b95ed8",
"reference": "ac3e079bbc86e95dc77d4f33965a62e9e6b95ed8",
"url": "https://api.github.com/repos/mollie/mollie-api-php/zipball/26010bd6999af4466c31c92733df87dc04d95772",
"reference": "26010bd6999af4466c31c92733df87dc04d95772",
"shasum": ""
},
"require": {
@ -4636,9 +4638,10 @@
"php": ">=5.6"
},
"require-dev": {
"eloquent/liberator": "^2.0",
"eloquent/liberator": "^2.0|^3.0",
"friendsofphp/php-cs-fixer": "^3.0",
"guzzlehttp/guzzle": "^6.3 || ^7.0",
"phpstan/phpstan": "^1.4",
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.1 || ^8.5 || ^9.5"
},
"suggest": {
@ -4701,9 +4704,9 @@
],
"support": {
"issues": "https://github.com/mollie/mollie-api-php/issues",
"source": "https://github.com/mollie/mollie-api-php/tree/v2.40.2"
"source": "https://github.com/mollie/mollie-api-php/tree/v2.41.0"
},
"time": "2022-02-08T08:53:18+00:00"
"time": "2022-03-24T15:06:33+00:00"
},
{
"name": "moneyphp/money",
@ -7887,16 +7890,16 @@
},
{
"name": "stripe/stripe-php",
"version": "v7.117.0",
"version": "v7.119.0",
"source": {
"type": "git",
"url": "https://github.com/stripe/stripe-php.git",
"reference": "c9d40524c63f3c5042d704f88a60f49a349b1221"
"reference": "a454dde82f5698cc4bcc5016c5328f533f516690"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/c9d40524c63f3c5042d704f88a60f49a349b1221",
"reference": "c9d40524c63f3c5042d704f88a60f49a349b1221",
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/a454dde82f5698cc4bcc5016c5328f533f516690",
"reference": "a454dde82f5698cc4bcc5016c5328f533f516690",
"shasum": ""
},
"require": {
@ -7941,9 +7944,9 @@
],
"support": {
"issues": "https://github.com/stripe/stripe-php/issues",
"source": "https://github.com/stripe/stripe-php/tree/v7.117.0"
"source": "https://github.com/stripe/stripe-php/tree/v7.119.0"
},
"time": "2022-03-18T18:16:15+00:00"
"time": "2022-03-25T20:09:27+00:00"
},
{
"name": "swiftmailer/swiftmailer",
@ -14615,16 +14618,16 @@
},
{
"name": "swagger-api/swagger-ui",
"version": "v4.6.2",
"version": "v4.9.1",
"source": {
"type": "git",
"url": "https://github.com/swagger-api/swagger-ui.git",
"reference": "d191c1ca18443a77273e9912ac2603ce1f7dc216"
"reference": "56fe8a1c279be6b55cd9aef7ce5e06e0ec364880"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/swagger-api/swagger-ui/zipball/d191c1ca18443a77273e9912ac2603ce1f7dc216",
"reference": "d191c1ca18443a77273e9912ac2603ce1f7dc216",
"url": "https://api.github.com/repos/swagger-api/swagger-ui/zipball/56fe8a1c279be6b55cd9aef7ce5e06e0ec364880",
"reference": "56fe8a1c279be6b55cd9aef7ce5e06e0ec364880",
"shasum": ""
},
"type": "library",
@ -14670,9 +14673,9 @@
],
"support": {
"issues": "https://github.com/swagger-api/swagger-ui/issues",
"source": "https://github.com/swagger-api/swagger-ui/tree/v4.6.2"
"source": "https://github.com/swagger-api/swagger-ui/tree/v4.9.1"
},
"time": "2022-03-10T11:36:38+00:00"
"time": "2022-03-25T18:33:52+00:00"
},
{
"name": "symfony/debug",
@ -14999,16 +15002,16 @@
},
{
"name": "zircote/swagger-php",
"version": "4.2.12",
"version": "4.2.13",
"source": {
"type": "git",
"url": "https://github.com/zircote/swagger-php.git",
"reference": "4ad0dc2245b6b603d732630fbea251ac92c630f2"
"reference": "8888655d7dc21eda6ec71e521f71a757605f48fe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/4ad0dc2245b6b603d732630fbea251ac92c630f2",
"reference": "4ad0dc2245b6b603d732630fbea251ac92c630f2",
"url": "https://api.github.com/repos/zircote/swagger-php/zipball/8888655d7dc21eda6ec71e521f71a757605f48fe",
"reference": "8888655d7dc21eda6ec71e521f71a757605f48fe",
"shasum": ""
},
"require": {
@ -15068,9 +15071,9 @@
],
"support": {
"issues": "https://github.com/zircote/swagger-php/issues",
"source": "https://github.com/zircote/swagger-php/tree/4.2.12"
"source": "https://github.com/zircote/swagger-php/tree/4.2.13"
},
"time": "2022-03-15T20:43:30+00:00"
"time": "2022-03-23T08:54:27+00:00"
}
],
"aliases": [],
@ -15090,5 +15093,5 @@
"platform-dev": {
"php": "^7.4|^8.0"
},
"plugin-api-version": "2.1.0"
"plugin-api-version": "2.0.0"
}

View File

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

View File

@ -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()
{
//
}
}

View File

@ -5779,6 +5779,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
cross_file
flutter_lints
flutter_plugin_android_lifecycle
google_sign_in
google_sign_in_platform_interface
@ -14632,6 +14633,37 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
lints
Copyright 2021, the Dart project authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
material_design_icons_flutter

View File

@ -25,14 +25,14 @@ const RESOURCES = {
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "015400679694f1f51047e46da0e1dc98",
"assets/AssetManifest.json": "38d9aea341601f3a5c6fa7b5a1216ea5",
"assets/NOTICES": "9a4bf0423a5e265f38c4df37f7a0a913",
"assets/NOTICES": "6820525c3248a35b97051cac66bfeed4",
"assets/fonts/MaterialIcons-Regular.otf": "7e7a6cccddf6d7b20012a548461d5d81",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"/": "872d6fc8570c28da77a84d81d0d344c7",
"/": "ceb3b368aacf6f0124e5e6d7c76b5b89",
"version.json": "443986d36b3df952ad780139ecccd516",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"main.dart.js": "f26fb24c7525ef96997b0d6b02504548",
"main.dart.js": "6043add2238de74021ea2fb01f9aec7b",
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40",
"canvaskit/profiling/canvaskit.js": "ae2949af4efc61d28a4a80fffa1db900",

39552
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

60564
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

38368
public/main.html.dart.js vendored

File diff suppressed because one or more lines are too long

60686
public/main.next.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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;

View File

@ -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>

View File

@ -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>

View 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);
}
}