1
0
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:
David Bomba 2022-03-26 13:20:52 +11:00 committed by GitHub
commit 5a89283cb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1366 additions and 158 deletions

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

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

View File

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

View File

@ -71,6 +71,9 @@ class UpdateInvoicePayment
->updatePaidToDate($paid_amount)
->updateStatus()
->touchPdf()
->save();
$invoice->service()
->workFlow()
->save();

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

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

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

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