Merge branch 'v5-develop' into v5-stable

This commit is contained in:
David Bomba 2022-06-16 16:54:49 +10:00
commit e5595cf914
130 changed files with 466319 additions and 452796 deletions

View File

@ -61,3 +61,11 @@ SENTRY_LARAVEL_DSN=https://39389664f3f14969b4c43dadda00a40b@sentry2.invoicing.co
GOOGLE_PLAY_PACKAGE_NAME= GOOGLE_PLAY_PACKAGE_NAME=
APPSTORE_PASSWORD= APPSTORE_PASSWORD=
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
MICROSOFT_REDIRECT_URI=
APPLE_CLIENT_ID=
APPLE_CLIENT_SECRET=
APPLE_REDIRECT_URI=

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
/node_modules /node_modules
/public/hot /public/hot
/public/storage /public/storage
/public/react
/storage/*.key /storage/*.key
/vendor /vendor
/.idea /.idea

View File

@ -1 +1 @@
5.3.98 5.4.0

View File

@ -62,7 +62,7 @@ class EmailTemplateDefaults
case 'email_template_custom3': case 'email_template_custom3':
return self::emailInvoiceTemplate(); return self::emailInvoiceTemplate();
case 'email_template_purchase_order': case 'email_template_purchase_order':
return self::emailPurchaseOrderSubject(); return self::emailPurchaseOrderTemplate();
break; break;
/* Subject */ /* Subject */
@ -157,7 +157,7 @@ class EmailTemplateDefaults
public static function emailPurchaseOrderSubject() public static function emailPurchaseOrderSubject()
{ {
return ctrans('texts.purchase_order_subject', ['number' => '$number']); return ctrans('texts.purchase_order_subject', ['number' => '$number', 'account' => '$account']);
} }
public static function emailPurchaseOrderTemplate() public static function emailPurchaseOrderTemplate()

View File

@ -0,0 +1,51 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Events\PurchaseOrder;
use App\Models\Company;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Models\VendorContact;
use Illuminate\Queue\SerializesModels;
/**
* Class PurchaseOrderWasAccepted.
*/
class PurchaseOrderWasAccepted
{
use SerializesModels;
/**
* @var PurchaseOrder
*/
public $purchase_order;
public $company;
public $event_vars;
public $contact;
/**
* Create a new event instance.
*
* @param PurchaseOrder $purchase_order
* @param Company $company
* @param array $event_vars
*/
public function __construct(PurchaseOrder $purchase_order, VendorContact $contact, Company $company, array $event_vars)
{
$this->purchase_order = $purchase_order;
$this->contact = $contact;
$this->company = $company;
$this->event_vars = $event_vars;
}
}

View File

@ -13,6 +13,7 @@ namespace App\Events\PurchaseOrder;
use App\Models\Company; use App\Models\Company;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
/** /**
@ -25,7 +26,7 @@ class PurchaseOrderWasEmailed
/** /**
* @var PurchaseOrder * @var PurchaseOrder
*/ */
public $purchase_order; public $invitation;
public $company; public $company;
@ -38,9 +39,9 @@ class PurchaseOrderWasEmailed
* @param Company $company * @param Company $company
* @param array $event_vars * @param array $event_vars
*/ */
public function __construct(PurchaseOrder $purchase_order, Company $company, array $event_vars) public function __construct(PurchaseOrderInvitation $invitation, Company $company, array $event_vars)
{ {
$this->purchase_order = $purchase_order; $this->invitation = $invitation;
$this->company = $company; $this->company = $company;
$this->event_vars = $event_vars; $this->event_vars = $event_vars;
} }

View File

@ -13,6 +13,7 @@ namespace App\Events\PurchaseOrder;
use App\Models\Company; use App\Models\Company;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
/** /**
@ -25,7 +26,7 @@ class PurchaseOrderWasViewed
/** /**
* @var PurchaseOrder * @var PurchaseOrder
*/ */
public $purchase_order; public $invitation;
public $company; public $company;
@ -38,9 +39,9 @@ class PurchaseOrderWasViewed
* @param Company $company * @param Company $company
* @param array $event_vars * @param array $event_vars
*/ */
public function __construct(PurchaseOrder $purchase_order, Company $company, array $event_vars) public function __construct(PurchaseOrderInvitation $invitation, Company $company, array $event_vars)
{ {
$this->purchase_order = $purchase_order; $this->invitation = $invitation;
$this->company = $company; $this->company = $company;
$this->event_vars = $event_vars; $this->event_vars = $event_vars;
} }

View File

@ -222,6 +222,9 @@ class Handler extends ExceptionHandler
case 'user': case 'user':
$login = 'login'; $login = 'login';
break; break;
case 'vendor':
$login = 'vendor.catchall';
break;
default: default:
$login = 'default'; $login = 'default';
break; break;

View File

@ -12,9 +12,11 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\Account\CreateAccountRequest; use App\Http\Requests\Account\CreateAccountRequest;
use App\Http\Requests\Account\UpdateAccountRequest;
use App\Jobs\Account\CreateAccount; use App\Jobs\Account\CreateAccount;
use App\Models\Account; use App\Models\Account;
use App\Models\CompanyUser; use App\Models\CompanyUser;
use App\Transformers\AccountTransformer;
use App\Transformers\CompanyUserTransformer; use App\Transformers\CompanyUserTransformer;
use App\Utils\TruthSource; use App\Utils\TruthSource;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
@ -157,4 +159,22 @@ class AccountController extends BaseController
return $this->listResponse($ct); return $this->listResponse($ct);
} }
public function update(UpdateAccountRequest $request, Account $account)
{
$fi = new \FilesystemIterator(public_path('react'), \FilesystemIterator::SKIP_DOTS);
if(iterator_count($fi) < 30)
return response()->json(['message' => 'React App Not Installed, Please install the React app before attempting to switch.'], 400);
$account->fill($request->all());
$account->save();
$this->entity_type = Account::class;
$this->entity_transformer = AccountTransformer::class;
return $this->itemResponse($account);
}
} }

View File

@ -92,7 +92,7 @@ class LoginController extends BaseController
* @return void * @return void
* deprecated .1 API ONLY we don't need to set any session variables * deprecated .1 API ONLY we don't need to set any session variables
*/ */
public function authenticated(Request $request, User $user) : void public function authenticated(Request $request, User $user): void
{ {
//$this->setCurrentCompanyId($user->companies()->first()->account->default_company_id); //$this->setCurrentCompanyId($user->companies()->first()->account->default_company_id);
} }
@ -168,9 +168,9 @@ class LoginController extends BaseController
$this->fireLockoutEvent($request); $this->fireLockoutEvent($request);
return response() return response()
->json(['message' => 'Too many login attempts, you are being throttled'], 401) ->json(['message' => 'Too many login attempts, you are being throttled'], 401)
->header('X-App-Version', config('ninja.app_version')) ->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version')); ->header('X-Api-Version', config('ninja.minimum_client_version'));
} }
if ($this->attemptLogin($request)) { if ($this->attemptLogin($request)) {
@ -196,7 +196,7 @@ class LoginController extends BaseController
} }
elseif($user->google_2fa_secret && !$request->has('one_time_password')) { elseif($user->google_2fa_secret && !$request->has('one_time_password')) {
return response() return response()
->json(['message' => ctrans('texts.invalid_one_time_password')], 401) ->json(['message' => ctrans('texts.invalid_one_time_password')], 401)
->header('X-App-Version', config('ninja.app_version')) ->header('X-App-Version', config('ninja.app_version'))
@ -234,23 +234,23 @@ class LoginController extends BaseController
// /* Ensure the user has a valid token */ // /* Ensure the user has a valid token */
// if($user->company_users()->count() != $user->tokens()->count()) // if($user->company_users()->count() != $user->tokens()->count())
// { // {
// $user->companies->each(function($company) use($user, $request){ // $user->companies->each(function($company) use($user, $request){
// if(!CompanyToken::where('user_id', $user->id)->where('company_id', $company->id)->exists()){ // if(!CompanyToken::where('user_id', $user->id)->where('company_id', $company->id)->exists()){
// CreateCompanyToken::dispatchNow($company, $user, $request->server('HTTP_USER_AGENT')); // CreateCompanyToken::dispatchNow($company, $user, $request->server('HTTP_USER_AGENT'));
// } // }
// }); // });
// } // }
// $truth->setCompanyToken(CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $user->account->default_company->id)->first()); // $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*/ /*On the hosted platform, only owners can login for free/pro accounts*/
if(Ninja::isHosted() && !$cu->first()->is_owner && !$user->account->isEnterpriseClient()) 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); return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403);
event(new UserLoggedIn($user, $user->account->default_company, Ninja::eventVars($user->id))); event(new UserLoggedIn($user, $user->account->default_company, Ninja::eventVars($user->id)));
@ -267,9 +267,9 @@ class LoginController extends BaseController
$this->incrementLoginAttempts($request); $this->incrementLoginAttempts($request);
return response() return response()
->json(['message' => ctrans('texts.invalid_credentials')], 401) ->json(['message' => ctrans('texts.invalid_credentials')], 401)
->header('X-App-Version', config('ninja.app_version')) ->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version')); ->header('X-Api-Version', config('ninja.minimum_client_version'));
} }
} }
@ -317,29 +317,28 @@ class LoginController extends BaseController
{ {
$truth = app()->make(TruthSource::class); $truth = app()->make(TruthSource::class);
if($truth->getCompanyToken()) if ($truth->getCompanyToken())
$company_token = $truth->getCompanyToken(); $company_token = $truth->getCompanyToken();
else else
$company_token = CompanyToken::where('token', $request->header('X-API-TOKEN'))->first(); $company_token = CompanyToken::where('token', $request->header('X-API-TOKEN'))->first();
$cu = CompanyUser::query() $cu = CompanyUser::query()
->where('user_id', $company_token->user_id); ->where('user_id', $company_token->user_id);
if($cu->count() == 0) if ($cu->count() == 0)
return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400);
$cu->first()->account->companies->each(function ($company) use($cu, $request){ $cu->first()->account->companies->each(function ($company) use ($cu, $request) {
if($company->tokens()->where('is_system', true)->count() == 0) if ($company->tokens()->where('is_system', true)->count() == 0) {
{
CreateCompanyToken::dispatchNow($company, $cu->first()->user, $request->server('HTTP_USER_AGENT')); CreateCompanyToken::dispatchNow($company, $cu->first()->user, $request->server('HTTP_USER_AGENT'));
} }
}); });
if($request->has('current_company') && $request->input('current_company') == 'true') if ($request->has('current_company') && $request->input('current_company') == 'true')
$cu->where("company_id", $company_token->company_id); $cu->where("company_id", $company_token->company_id);
if(Ninja::isHosted() && !$cu->first()->is_owner && !$cu->first()->user->account->isEnterpriseClient()) if (Ninja::isHosted() && !$cu->first()->is_owner && !$cu->first()->user->account->isEnterpriseClient())
return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403);
return $this->refreshResponse($cu); return $this->refreshResponse($cu);
@ -359,24 +358,134 @@ class LoginController extends BaseController
*/ */
public function oauthApiLogin() public function oauthApiLogin()
{ {
$message = 'Provider not supported';
if (request()->input('provider') == 'google') { if (request()->input('provider') == 'google') {
return $this->handleGoogleOauth(); return $this->handleGoogleOauth();
} elseif (request()->input('provider') == 'microsoft') {
if (request()->has('token')) {
return $this->handleSocialiteLogin('microsoft', request()->get('token'));
} else {
$message = 'Bearer token missing for the microsoft login';
}
} elseif (request()->input('provider') == 'apple') {
if (request()->has('token')) {
return $this->handleSocialiteLogin('apple', request()->get('token'));
} else {
$message = 'Token is missing for the apple login';
}
} }
return response() return response()
->json(['message' => 'Provider not supported'], 400) ->json(['message' => $message], 400)
->header('X-App-Version', config('ninja.app_version')) ->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version')); ->header('X-Api-Version', config('ninja.minimum_client_version'));
} }
private function hydrateCompanyUser() :Builder private function getSocialiteUser(string $provider, string $token)
{
return Socialite::driver($provider)->userFromToken($token);
}
private function handleSocialiteLogin($provider, $token)
{
$user = $this->getSocialiteUser($provider, $token);
if ($user) {
return $this->loginOrCreateFromSocialite($user, $provider);
}
return response()
->json(['message' => ctrans('texts.invalid_credentials')], 401)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
private function loginOrCreateFromSocialite($user, $provider)
{
$query = [
'oauth_user_id' => $user->id,
'oauth_provider_id' => $provider,
];
if ($existing_user = MultiDB::hasUser($query)) {
if (!$existing_user->account)
return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400);
Auth::login($existing_user, true);
$cu = $this->hydrateCompanyUser();
if ($cu->count() == 0)
return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400);
if (Ninja::isHosted() && !$cu->first()->is_owner && !$existing_user->account->isEnterpriseClient())
return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403);
return $this->timeConstrainedResponse($cu);
}
//If this is a result user/email combo - lets add their OAuth details details
if ($existing_login_user = MultiDB::hasUser(['email' => $user->email])) {
if (!$existing_login_user->account)
return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400);
Auth::login($existing_login_user, true);
auth()->user()->update([
'oauth_user_id' => $user->id,
'oauth_provider_id' => $provider,
]);
$cu = $this->hydrateCompanyUser();
if ($cu->count() == 0)
return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400);
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);
return $this->timeConstrainedResponse($cu);
}
$name = OAuth::splitName($user->name);
$new_account = [
'first_name' => $name[0],
'last_name' => $name[1],
'password' => '',
'email' => $user->email,
'oauth_user_id' => $user->id,
'oauth_provider_id' => $provider,
];
MultiDB::setDefaultDatabase();
$account = CreateAccount::dispatchNow($new_account, request()->getClientIp());
Auth::login($account->default_company->owner(), true);
auth()->user()->email_verified_at = now();
auth()->user()->save();
$cu = $this->hydrateCompanyUser();
if ($cu->count() == 0)
return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400);
if (Ninja::isHosted() && !$cu->first()->is_owner && !auth()->user()->account->isEnterpriseClient())
return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403);
return $this->timeConstrainedResponse($cu);
}
private function hydrateCompanyUser(): Builder
{ {
$cu = CompanyUser::query()->where('user_id', auth()->user()->id); $cu = CompanyUser::query()->where('user_id', auth()->user()->id);
if(CompanyUser::query()->where('user_id', auth()->user()->id)->where('company_id', auth()->user()->account->default_company_id)->exists()) if (CompanyUser::query()->where('user_id', auth()->user()->id)->where('company_id', auth()->user()->account->default_company_id)->exists())
$set_company = auth()->user()->account->default_company; $set_company = auth()->user()->account->default_company;
else{ else {
$set_company = $cu->first()->company; $set_company = $cu->first()->company;
} }
@ -392,19 +501,18 @@ class LoginController extends BaseController
if($cu->count() == 0) if($cu->count() == 0)
return $cu; return $cu;
if(auth()->user()->company_users()->count() != auth()->user()->tokens()->distinct('company_id')->count()) if (auth()->user()->company_users()->count() != auth()->user()->tokens()->distinct('company_id')->count()) {
{
auth()->user()->companies->each(function ($company) {
auth()->user()->companies->each(function($company){
if (!CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $company->id)->exists()) {
if(!CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $company->id)->exists()){
CreateCompanyToken::dispatchNow($company, auth()->user(), "Google_O_Auth");
CreateCompanyToken::dispatchNow($company, auth()->user(), "Google_O_Auth");
} }
}); });
} }
$truth->setCompanyToken(CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $set_company->id)->first()); $truth->setCompanyToken(CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $set_company->id)->first());
@ -444,7 +552,7 @@ class LoginController extends BaseController
return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403);
return $this->timeConstrainedResponse($cu); return $this->timeConstrainedResponse($cu);
} }
//If this is a result user/email combo - lets add their OAuth details details //If this is a result user/email combo - lets add their OAuth details details
@ -474,14 +582,14 @@ class LoginController extends BaseController
} }
if ($user) { if ($user) {
//check the user doesn't already exist in some form //check the user doesn't already exist in some form
if($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user)])) if($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user)]))
{ {
if(!$existing_login_user->account) if(!$existing_login_user->account)
return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400); return response()->json(['message' => 'User exists, but not attached to any companies! Orphaned user!'], 400);
Auth::login($existing_login_user, true); Auth::login($existing_login_user, true);
auth()->user()->update([ auth()->user()->update([
@ -490,11 +598,11 @@ class LoginController extends BaseController
]); ]);
$cu = $this->hydrateCompanyUser(); $cu = $this->hydrateCompanyUser();
// $cu = CompanyUser::query() // $cu = CompanyUser::query()
// ->where('user_id', auth()->user()->id); // ->where('user_id', auth()->user()->id);
if($cu->count() == 0) if ($cu->count() == 0)
return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400);
if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterpriseClient()) if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterpriseClient())
@ -557,7 +665,7 @@ class LoginController extends BaseController
if (request()->has('code')) { if (request()->has('code')) {
return $this->handleProviderCallback($provider); return $this->handleProviderCallback($provider);
} else { } else {
if(!in_array($provider, ['google'])) if(!in_array($provider, ['google']))
return abort(400, 'Invalid provider'); return abort(400, 'Invalid provider');
@ -594,7 +702,7 @@ class LoginController extends BaseController
'oauth_user_id' => $socialite_user->getId(), 'oauth_user_id' => $socialite_user->getId(),
'oauth_provider_id' => $provider, 'oauth_provider_id' => $provider,
'oauth_user_token' => $oauth_user_token, 'oauth_user_token' => $oauth_user_token,
'oauth_user_refresh_token' => $socialite_user->refreshToken 'oauth_user_refresh_token' => $socialite_user->refreshToken
]; ];
$user->update($update_user); $user->update($update_user);

View File

@ -0,0 +1,58 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\Auth;
use App\Events\Contact\ContactLoggedIn;
use App\Http\Controllers\Controller;
use App\Http\ViewComposers\PortalComposer;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\ClientContact;
use App\Models\Company;
use App\Utils\Ninja;
use Auth;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Route;
class VendorContactLoginController extends Controller
{
use AuthenticatesUsers;
protected $redirectTo = '/vendor/purchase_orders';
public function __construct()
{
$this->middleware('guest:vendor', ['except' => ['logout']]);
}
public function catch()
{
$data = [
];
return $this->render('purchase_orders.catch');
}
public function logout()
{
Auth::guard('vendor')->logout();
request()->session()->invalidate();
return redirect('/vendors');
}
}

View File

@ -84,6 +84,7 @@ class BaseController extends Controller
'company.products.documents', 'company.products.documents',
'company.payments.paymentables', 'company.payments.paymentables',
'company.payments.documents', 'company.payments.documents',
'company.purchase_orders.documents',
'company.payment_terms.company', 'company.payment_terms.company',
'company.projects.documents', 'company.projects.documents',
'company.recurring_expenses', 'company.recurring_expenses',
@ -171,7 +172,12 @@ class BaseController extends Controller
*/ */
public function notFoundClient() public function notFoundClient()
{ {
abort(404, 'Page not found in client portal.'); abort(404, 'Page not found in the client portal.');
}
public function notFoundVendor()
{
abort(404, 'Page not found in the vendor portal.');
} }
/** /**
@ -296,6 +302,13 @@ class BaseController extends Controller
if(!$user->hasPermission('view_project')) if(!$user->hasPermission('view_project'))
$query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id); $query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id);
},
'company.purchase_orders'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_purchase_order'))
$query->where('purchase_orders.user_id', $user->id)->orWhere('purchase_orders.assigned_user_id', $user->id);
}, },
'company.quotes'=> function ($query) use ($updated_at, $user) { 'company.quotes'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents'); $query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
@ -533,6 +546,13 @@ class BaseController extends Controller
if(!$user->hasPermission('view_project')) if(!$user->hasPermission('view_project'))
$query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id); $query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id);
},
'company.purchase_orders'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('documents');
if(!$user->hasPermission('view_purchase_order'))
$query->where('purchase_orders.user_id', $user->id)->orWhere('purchase_orders.assigned_user_id', $user->id);
}, },
'company.quotes'=> function ($query) use ($created_at, $user) { 'company.quotes'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('invitations', 'documents'); $query->where('created_at', '>=', $created_at)->with('invitations', 'documents');
@ -780,7 +800,7 @@ class BaseController extends Controller
$this->buildCache(); $this->buildCache();
if(config('ninja.react_app_enabled')) if(Ninja::isSelfHost() && $account->set_react_as_default_ap)
return response()->view('react.index', $data)->header('X-Frame-Options', 'SAMEORIGIN', false); return response()->view('react.index', $data)->header('X-Frame-Options', 'SAMEORIGIN', false);
else else
return response()->view('index.index', $data)->header('X-Frame-Options', 'SAMEORIGIN', false); return response()->view('index.index', $data)->header('X-Frame-Options', 'SAMEORIGIN', false);

View File

@ -154,9 +154,11 @@ class EntityViewController extends Controller
if (! $invitation->viewed_date) { if (! $invitation->viewed_date) {
$invitation->markViewed(); $invitation->markViewed();
event(new InvitationWasViewed($invitation->{$request->entity_type}, $invitation, $invitation->{$request->entity_type}->company, Ninja::eventVars())); if(!session()->get('is_silent'))
event(new InvitationWasViewed($invitation->{$request->entity_type}, $invitation, $invitation->{$request->entity_type}->company, Ninja::eventVars()));
$this->fireEntityViewedEvent($invitation, $request->entity_type); if(!session()->get('is_silent'))
$this->fireEntityViewedEvent($invitation, $request->entity_type);
} }
return redirect()->route('client.'.$request->entity_type.'.show', [$request->entity_type => $this->encodePrimaryKey($invitation->{$key})]); return redirect()->route('client.'.$request->entity_type.'.show', [$request->entity_type => $this->encodePrimaryKey($invitation->{$key})]);

View File

@ -129,9 +129,11 @@ class InvitationController extends Controller
if (auth()->guard('contact')->user() && ! request()->has('silent') && ! $invitation->viewed_date) { if (auth()->guard('contact')->user() && ! request()->has('silent') && ! $invitation->viewed_date) {
$invitation->markViewed(); $invitation->markViewed();
event(new InvitationWasViewed($invitation->{$entity}, $invitation, $invitation->{$entity}->company, Ninja::eventVars())); if(!session()->get('is_silent'))
event(new InvitationWasViewed($invitation->{$entity}, $invitation, $invitation->{$entity}->company, Ninja::eventVars()));
$this->fireEntityViewedEvent($invitation, $entity); if(!session()->get('is_silent'))
$this->fireEntityViewedEvent($invitation, $entity);
} }
else{ else{
$is_silent = 'true'; $is_silent = 'true';

View File

@ -61,7 +61,7 @@ class InvoiceController extends Controller
$invitation = $invoice->invitations()->where('client_contact_id', auth()->guard('contact')->user()->id)->first(); $invitation = $invoice->invitations()->where('client_contact_id', auth()->guard('contact')->user()->id)->first();
if ($invitation && auth()->guard('contact') && ! request()->has('silent') && ! $invitation->viewed_date) { if ($invitation && auth()->guard('contact') && !session()->get('is_silent') && ! $invitation->viewed_date) {
$invitation->markViewed(); $invitation->markViewed();

View File

@ -154,6 +154,7 @@ class NinjaPlanController extends Controller
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill); $recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
$recurring_invoice->due_date_days = 'terms'; $recurring_invoice->due_date_days = 'terms';
$recurring_invoice->next_send_date = now()->addDays(14)->format('Y-m-d'); $recurring_invoice->next_send_date = now()->addDays(14)->format('Y-m-d');
$recurring_invoice->next_send_date_client = now()->addDays(14)->format('Y-m-d');
$recurring_invoice->save(); $recurring_invoice->save();
$r = $recurring_invoice->calc()->getRecurringInvoice(); $r = $recurring_invoice->calc()->getRecurringInvoice();

View File

@ -24,6 +24,8 @@ use App\Http\Requests\PurchaseOrder\ShowPurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\StorePurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\StorePurchaseOrderRequest;
use App\Http\Requests\PurchaseOrder\UpdatePurchaseOrderRequest; use App\Http\Requests\PurchaseOrder\UpdatePurchaseOrderRequest;
use App\Jobs\Invoice\ZipInvoices; use App\Jobs\Invoice\ZipInvoices;
use App\Jobs\PurchaseOrder\PurchaseOrderEmail;
use App\Jobs\PurchaseOrder\ZipPurchaseOrders;
use App\Models\Client; use App\Models\Client;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Repositories\PurchaseOrderRepository; use App\Repositories\PurchaseOrderRepository;
@ -31,6 +33,7 @@ use App\Transformers\PurchaseOrderTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
class PurchaseOrderController extends BaseController class PurchaseOrderController extends BaseController
{ {
@ -183,6 +186,7 @@ class PurchaseOrderController extends BaseController
$purchase_order = $purchase_order->service() $purchase_order = $purchase_order->service()
->fillDefaults() ->fillDefaults()
->triggeredActions($request)
->save(); ->save();
event(new PurchaseOrderWasCreated($purchase_order, $purchase_order->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); event(new PurchaseOrderWasCreated($purchase_order, $purchase_order->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
@ -485,7 +489,7 @@ class PurchaseOrderController extends BaseController
* Download Purchase Order/s * Download Purchase Order/s
*/ */
if ($action == 'bulk_download' && $purchase_orders->count() > 1) { if ($action == 'bulk_download' && $purchase_orders->count() >= 1) {
$purchase_orders->each(function ($purchase_order) { $purchase_orders->each(function ($purchase_order) {
if (auth()->user()->cannot('view', $purchase_order)) { if (auth()->user()->cannot('view', $purchase_order)) {
nlog("access denied"); nlog("access denied");
@ -493,7 +497,7 @@ class PurchaseOrderController extends BaseController
} }
}); });
ZipInvoices::dispatch($purchase_orders, $purchase_orders->first()->company, auth()->user()); ZipPurchaseOrders::dispatch($purchase_orders, $purchase_orders->first()->company, auth()->user());
return response()->json(['message' => ctrans('texts.sent_message')], 200); return response()->json(['message' => ctrans('texts.sent_message')], 200);
} }
@ -579,7 +583,7 @@ class PurchaseOrderController extends BaseController
*/ */
public function action(ActionPurchaseOrderRequest $request, PurchaseOrder $purchase_order, $action) public function action(ActionPurchaseOrderRequest $request, PurchaseOrder $purchase_order, $action)
{ {
return $this->performAction($invoice, $action); return $this->performAction($purchase_order, $action);
} }
private function performAction(PurchaseOrder $purchase_order, $action, $bulk = false) private function performAction(PurchaseOrder $purchase_order, $action, $bulk = false)
@ -627,8 +631,13 @@ class PurchaseOrderController extends BaseController
case 'email': case 'email':
//check query parameter for email_type and set the template else use calculateTemplate //check query parameter for email_type and set the template else use calculateTemplate
PurchaseOrderEmail::dispatch($purchase_order, $purchase_order->company);
if (! $bulk) {
return response()->json(['message' => 'email sent'], 200);
}
default: default:
return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400); return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400);
break; break;

View File

@ -135,6 +135,9 @@ class SelfUpdateController extends BaseController
nlog("Extracting zip"); nlog("Extracting zip");
//clean up old snappdf installations
$this->cleanOldSnapChromeBinaries();
// try{ // try{
// $s = new Snappdf; // $s = new Snappdf;
// $s->getChromiumPath(); // $s->getChromiumPath();
@ -190,6 +193,46 @@ class SelfUpdateController extends BaseController
} }
private function cleanOldSnapChromeBinaries()
{
$current_revision = base_path('vendor/beganovich/snappdf/versions/revision.txt');
$current_revision_text = file_get_contents($current_revision);
$iterator = new \DirectoryIterator(base_path('vendor/beganovich/snappdf/versions'));
foreach ($iterator as $file)
{
if($file->isDir() && !$file->isDot() && ($current_revision_text != $file->getFileName()))
{
$directoryIterator = new \RecursiveDirectoryIterator(base_path('vendor/beganovich/snappdf/versions/'.$file->getFileName()), \RecursiveDirectoryIterator::SKIP_DOTS);
foreach (new \RecursiveIteratorIterator($directoryIterator) as $filex)
{
unlink($filex->getPathName());
}
$this->deleteDirectory(base_path('vendor/beganovich/snappdf/versions/'.$file->getFileName()));
}
}
}
private function deleteDirectory($dir) {
if (!file_exists($dir)) return true;
if (!is_dir($dir) || is_link($dir)) return unlink($dir);
foreach (scandir($dir) as $item) {
if ($item == '.' || $item == '..') continue;
if (!$this->deleteDirectory($dir . "/" . $item)) {
if (!$this->deleteDirectory($dir . "/" . $item)) return false;
};
}
return rmdir($dir);
}
private function postHookUpdate() private function postHookUpdate()
{ {
if(config('ninja.app_version') == '5.3.82') if(config('ninja.app_version') == '5.3.82')

View File

@ -35,6 +35,7 @@ class SubdomainController extends BaseController
'html', 'html',
'lb', 'lb',
'shopify', 'shopify',
'beta',
]; ];
public function __construct() public function __construct()

View File

@ -0,0 +1,144 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\VendorPortal;
use App\Events\Credit\CreditWasViewed;
use App\Events\Invoice\InvoiceWasViewed;
use App\Events\Misc\InvitationWasViewed;
use App\Events\Quote\QuoteWasViewed;
use App\Http\Controllers\Controller;
use App\Jobs\Entity\CreateRawPdf;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Models\PurchaseOrderInvitation;
use App\Models\QuoteInvitation;
use App\Services\ClientPortal\InstantPayment;
use App\Utils\CurlUtils;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
/**
* Class InvitationController.
*/
class InvitationController extends Controller
{
use MakesHash;
use MakesDates;
public function purchaseOrder(string $invitation_key)
{
Auth::logout();
$invitation = PurchaseOrderInvitation::where('key', $invitation_key)
->whereHas('purchase_order', function ($query) {
$query->where('is_deleted',0);
})
->with('contact.vendor')
->first();
if(!$invitation)
return abort(404,'The resource is no longer available.');
if($invitation->contact->trashed())
$invitation->contact->restore();
$vendor_contact = $invitation->contact;
$entity = 'purchase_order';
if(empty($vendor_contact->email))
$vendor_contact->email = Str::random(15) . "@example.com"; $vendor_contact->save();
if (request()->has('vendor_hash') && request()->input('vendor_hash') == $invitation->contact->vendor->vendor_hash) {
request()->session()->invalidate();
auth()->guard('vendor')->loginUsingId($vendor_contact->id, true);
} else {
nlog("else - default - login contact");
request()->session()->invalidate();
auth()->guard('vendor')->loginUsingId($vendor_contact->id, true);
}
session()->put('is_silent', request()->has('silent'));
if (auth()->guard('vendor')->user() && ! session()->get('is_silent') && ! $invitation->viewed_date) {
$invitation->markViewed();
event(new InvitationWasViewed($invitation->purchase_order, $invitation, $invitation->company, Ninja::eventVars()));
}
else{
return redirect()->route('vendor.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->purchase_order_id), 'silent' => session()->get('is_silent')]);
}
return redirect()->route('vendor.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->purchase_order_id)]);
}
// public function routerForDownload(string $entity, string $invitation_key)
// {
// set_time_limit(45);
// if(Ninja::isHosted())
// return $this->returnRawPdf($entity, $invitation_key);
// return redirect('client/'.$entity.'/'.$invitation_key.'/download_pdf');
// }
// private function returnRawPdf(string $entity, string $invitation_key)
// {
// if(!in_array($entity, ['invoice', 'credit', 'quote', 'recurring_invoice']))
// return response()->json(['message' => 'Invalid resource request']);
// $key = $entity.'_id';
// $entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
// $invitation = $entity_obj::where('key', $invitation_key)
// ->with('contact.client')
// ->firstOrFail();
// if(!$invitation)
// return response()->json(["message" => "no record found"], 400);
// $file_name = $invitation->purchase_order->numberFormatter().'.pdf';
// $file = CreateRawPdf::dispatchNow($invitation, $invitation->company->db);
// $headers = ['Content-Type' => 'application/pdf'];
// if(request()->input('inline') == 'true')
// $headers = array_merge($headers, ['Content-Disposition' => 'inline']);
// return response()->streamDownload(function () use($file) {
// echo $file;
// }, $file_name, $headers);
// }
}

View File

@ -0,0 +1,229 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\VendorPortal;
use App\Events\Misc\InvitationWasViewed;
use App\Events\PurchaseOrder\PurchaseOrderWasAccepted;
use App\Events\PurchaseOrder\PurchaseOrderWasViewed;
use App\Http\Controllers\Controller;
use App\Http\Requests\VendorPortal\PurchaseOrders\ProcessPurchaseOrdersInBulkRequest;
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrderRequest;
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrdersRequest;
use App\Jobs\Invoice\InjectSignature;
use App\Models\PurchaseOrder;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
class PurchaseOrderController extends Controller
{
use MakesHash, MakesDates;
public const MODULE_RECURRING_INVOICES = 1;
public const MODULE_CREDITS = 2;
public const MODULE_QUOTES = 4;
public const MODULE_TASKS = 8;
public const MODULE_EXPENSES = 16;
public const MODULE_PROJECTS = 32;
public const MODULE_VENDORS = 64;
public const MODULE_TICKETS = 128;
public const MODULE_PROPOSALS = 256;
public const MODULE_RECURRING_EXPENSES = 512;
public const MODULE_RECURRING_TASKS = 1024;
public const MODULE_RECURRING_QUOTES = 2048;
public const MODULE_INVOICES = 4096;
public const MODULE_PROFORMAL_INVOICES = 8192;
public const MODULE_PURCHASE_ORDERS = 16384;
/**
* Display list of invoices.
*
* @return Factory|View
*/
public function index(ShowPurchaseOrdersRequest $request)
{
return $this->render('purchase_orders.index', ['company' => auth()->user()->company, 'settings' => auth()->user()->company->settings, 'sidebar' => $this->sidebarMenu()]);
}
/**
* Show specific invoice.
*
* @param ShowInvoiceRequest $request
* @param Invoice $invoice
*
* @return Factory|View
*/
public function show(ShowPurchaseOrderRequest $request, PurchaseOrder $purchase_order)
{
set_time_limit(0);
$invitation = $purchase_order->invitations()->where('vendor_contact_id', auth()->guard('vendor')->user()->id)->first();
if ($invitation && auth()->guard('vendor') && !session()->get('is_silent') && ! $invitation->viewed_date) {
$invitation->markViewed();
event(new InvitationWasViewed($purchase_order, $invitation, $purchase_order->company, Ninja::eventVars()));
event(new PurchaseOrderWasViewed($invitation, $invitation->company, Ninja::eventVars()));
}
$data = [
'purchase_order' => $purchase_order,
'key' => $invitation ? $invitation->key : false,
'settings' => $purchase_order->company->settings,
'sidebar' => $this->sidebarMenu(),
'company' => $purchase_order->company
];
if ($request->query('mode') === 'fullscreen') {
return render('purchase_orders.show-fullscreen', $data);
}
return $this->render('purchase_orders.show', $data);
}
private function sidebarMenu() :array
{
$enabled_modules = auth()->guard('vendor')->user()->company->enabled_modules;
$data = [];
// TODO: Enable dashboard once it's completed.
// $this->settings->enable_client_portal_dashboard
// $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
if (self::MODULE_PURCHASE_ORDERS & $enabled_modules) {
$data[] = ['title' => ctrans('texts.purchase_orders'), 'url' => 'vendor.purchase_orders.index', 'icon' => 'file-text'];
}
// $data[] = ['title' => ctrans('texts.documents'), 'url' => 'client.documents.index', 'icon' => 'download'];
return $data;
}
public function bulk(ProcessPurchaseOrdersInBulkRequest $request)
{
$transformed_ids = $this->transformKeys($request->purchase_orders);
if ($request->input('action') == 'download') {
return $this->downloadInvoices((array) $transformed_ids);
}
elseif ($request->input('action') == 'accept'){
return $this->acceptPurchaseOrder($request->all());
}
return redirect()
->back()
->with('message', ctrans('texts.no_action_provided'));
}
public function acceptPurchaseOrder($data)
{
$purchase_orders = PurchaseOrder::query()
->whereIn('id', $this->transformKeys($data['purchase_orders']))
->where('company_id', auth()->guard('vendor')->user()->vendor->company_id)
->whereIn('status_id', [PurchaseOrder::STATUS_DRAFT, PurchaseOrder::STATUS_SENT])
->cursor()->each(function ($purchase_order){
$purchase_order->service()
->markSent()
->applyNumber()
->setStatus(PurchaseOrder::STATUS_ACCEPTED)
->save();
if (request()->has('signature') && !is_null(request()->signature) && !empty(request()->signature)) {
InjectSignature::dispatch($purchase_order, request()->signature);
}
event(new PurchaseOrderWasAccepted($purchase_order, auth()->guard('vendor')->user(), $purchase_order->company, Ninja::eventVars()));
});
if(count($data['purchase_orders']) == 1){
$purchase_order = PurchaseOrder::whereIn('id', $this->transformKeys($data['purchase_orders']))->first();
return redirect()->route('vendor.purchase_order.show', ['purchase_order' => $purchase_order->hashed_id]);
}
else
return redirect()->route('vendor.purchase_orders.index');
}
public function downloadInvoices($ids)
{
$purchase_orders = PurchaseOrder::whereIn('id', $ids)
->where('vendor_id', auth()->guard('vendor')->user()->vendor_id)
->withTrashed()
->get();
if(count($purchase_orders) == 0)
return back()->with(['message' => ctrans('texts.no_items_selected')]);
if(count($purchase_orders) == 1){
$purchase_order = $purchase_orders->first();
$file = $purchase_order->service()->getPurchaseOrderPdf(auth()->guard('vendor')->user());
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file), ['Content-Type' => 'application/pdf']);
}
return $this->buildZip($purchase_orders);
}
private function buildZip($purchase_orders)
{
// create new archive
$zipFile = new \PhpZip\ZipFile();
try{
foreach ($purchase_orders as $purchase_order) {
#add it to the zip
$zipFile->addFromString(basename($purchase_order->pdf_file_path()), file_get_contents($purchase_order->pdf_file_path(null, 'url', true)));
}
$filename = date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.purchase_orders')).'.zip';
$filepath = sys_get_temp_dir() . '/' . $filename;
$zipFile->saveAsFile($filepath) // save the archive to a file
->close(); // close archive
return response()->download($filepath, $filename)->deleteFileAfterSend(true);
}
catch(\PhpZip\Exception\ZipException $e){
// handle exception
}
finally{
$zipFile->close();
}
}
}

View File

@ -0,0 +1,81 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\VendorPortal;
use App\Http\Controllers\Controller;
use App\Models\VendorContact;
use App\Utils\Traits\MakesHash;
use App\Utils\TranslationHelper;
use Illuminate\Http\Request;
class VendorContactController extends Controller
{
use MakesHash;
public const MODULE_RECURRING_INVOICES = 1;
public const MODULE_CREDITS = 2;
public const MODULE_QUOTES = 4;
public const MODULE_TASKS = 8;
public const MODULE_EXPENSES = 16;
public const MODULE_PROJECTS = 32;
public const MODULE_VENDORS = 64;
public const MODULE_TICKETS = 128;
public const MODULE_PROPOSALS = 256;
public const MODULE_RECURRING_EXPENSES = 512;
public const MODULE_RECURRING_TASKS = 1024;
public const MODULE_RECURRING_QUOTES = 2048;
public const MODULE_INVOICES = 4096;
public const MODULE_PROFORMAL_INVOICES = 8192;
public const MODULE_PURCHASE_ORDERS = 16384;
public function edit(VendorContact $vendor_contact)
{
return $this->render('vendor_profile.edit', [
'contact' => $vendor_contact,
'vendor' => $vendor_contact->vendor,
'settings' => $vendor_contact->vendor->company->settings,
'company' => $vendor_contact->vendor->company,
'sidebar' => $this->sidebarMenu(),
'countries' => TranslationHelper::getCountries()
]);
}
public function update(VendorContact $vendor_contact)
{
$vendor_contact->fill(request()->all());
$vendor_contact->vendor->fill(request()->all());
$vendor_contact->push();
return back()->withSuccess(ctrans('texts.profile_updated_successfully'));
}
private function sidebarMenu() :array
{
$enabled_modules = auth()->guard('vendor')->user()->company->enabled_modules;
$data = [];
// TODO: Enable dashboard once it's completed.
// $this->settings->enable_client_portal_dashboard
// $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
if (self::MODULE_PURCHASE_ORDERS & $enabled_modules) {
$data[] = ['title' => ctrans('texts.purchase_orders'), 'url' => 'vendor.purchase_orders.index', 'icon' => 'file-text'];
}
// $data[] = ['title' => ctrans('texts.documents'), 'url' => 'client.documents.index', 'icon' => 'download'];
return $data;
}
}

View File

@ -29,7 +29,7 @@ class WePayController extends BaseController
*/ */
public function signup(string $token) public function signup(string $token)
{ {
return render('gateways.wepay.signup.finished'); // return render('gateways.wepay.signup.finished');
$hash = Cache::get($token); $hash = Cache::get($token);

View File

@ -42,6 +42,7 @@ use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustProxies; use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\UrlSetDb; use App\Http\Middleware\UrlSetDb;
use App\Http\Middleware\UserVerified; use App\Http\Middleware\UserVerified;
use App\Http\Middleware\VendorLocale;
use App\Http\Middleware\VerifyCsrfToken; use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize; use Illuminate\Auth\Middleware\Authorize;
@ -158,6 +159,7 @@ class Kernel extends HttpKernel
'api_db' => SetDb::class, 'api_db' => SetDb::class,
'company_key_db' => SetDbByCompanyKey::class, 'company_key_db' => SetDbByCompanyKey::class,
'locale' => Locale::class, 'locale' => Locale::class,
'vendor_locale' => VendorLocale::class,
'contact_register' => ContactRegister::class, 'contact_register' => ContactRegister::class,
'shop_token_auth' => ShopTokenAuth::class, 'shop_token_auth' => ShopTokenAuth::class,
'phantom_secret' => PhantomSecret::class, 'phantom_secret' => PhantomSecret::class,

View File

@ -0,0 +1,76 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Livewire;
use App\Libraries\MultiDB;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Utils\Traits\WithSorting;
use Carbon\Carbon;
use Livewire\Component;
use Livewire\WithPagination;
class PurchaseOrdersTable extends Component
{
use WithPagination, WithSorting;
public $per_page = 10;
public $status = [];
public $company;
public function mount()
{
MultiDB::setDb($this->company->db);
$this->sort_asc = false;
$this->sort_field = 'date';
}
public function render()
{
$local_status = [];
$query = PurchaseOrder::query()
->with('vendor.contacts')
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->whereIn('status_id', [PurchaseOrder::STATUS_SENT, PurchaseOrder::STATUS_ACCEPTED])
->where('company_id', $this->company->id)
->where('is_deleted', false);
if (in_array('sent', $this->status)) {
$local_status[] = PurchaseOrder::STATUS_SENT;
}
if (in_array('accepted', $this->status)) {
$local_status[] = PurchaseOrder::STATUS_ACCEPTED;
}
if (count($local_status) > 0) {
$query = $query->whereIn('status_id', array_unique($local_status));
}
$query = $query
->where('vendor_id', auth()->guard('vendor')->user()->vendor_id)
// ->where('status_id', '<>', Invoice::STATUS_DRAFT)
// ->where('status_id', '<>', Invoice::STATUS_CANCELLED)
->withTrashed()
->paginate($this->per_page);
return render('components.livewire.purchase-orders-table', [
'purchase_orders' => $query
]);
}
}

View File

@ -60,6 +60,8 @@ class CheckClientExistence
session()->put('multiple_contacts', $multiple_contacts); session()->put('multiple_contacts', $multiple_contacts);
session()->put('is_silent', request()->has('silent'));
return $next($request); return $next($request);
} }
} }

View File

@ -20,32 +20,38 @@ class RedirectIfAuthenticated
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param Request $request * @param Request $request
* @param Closure $next * @param Closure $next
* @param string|null $guard * @param string|null $guard
* @return mixed * @return mixed
*/ */
public function handle($request, Closure $next, $guard = null) public function handle($request, Closure $next, $guard = null)
{ {
switch ($guard) { switch ($guard) {
case 'contact': case 'contact':
if (Auth::guard($guard)->check()) { if (Auth::guard($guard)->check()) {
return redirect()->route('client.dashboard'); return redirect()->route('client.dashboard');
} }
break; break;
case 'user': case 'user':
Auth::logout(); Auth::logout();
// if (Auth::guard($guard)->check()) { // if (Auth::guard($guard)->check()) {
// return redirect()->route('dashboard.index'); // return redirect()->route('dashboard.index');
// } // }
break; break;
default: case 'vendor':
Auth::logout(); if (Auth::guard($guard)->check()) {
// if (Auth::guard($guard)->check()) { //TODO create routes for vendor
// return redirect('/'); // return redirect()->route('vendor.dashboard');
// } }
break; break;
} default:
Auth::logout();
// if (Auth::guard($guard)->check()) {
// return redirect('/');
// }
break;
}
return $next($request); return $next($request);
} }

View File

@ -46,7 +46,7 @@ class SetInviteDb
if($entity == "pay") if($entity == "pay")
$entity = "invoice"; $entity = "invoice";
if(!in_array($entity, ['invoice','quote','credit','recurring_invoice'])) if(!in_array($entity, ['invoice','quote','credit','recurring_invoice','purchase_order']))
abort(404,'I could not find this resource.'); abort(404,'I could not find this resource.');
/* Try and determine the DB from the invitation key STRING*/ /* Try and determine the DB from the invitation key STRING*/

View File

@ -0,0 +1,57 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
class VendorLocale
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (auth()->guard('contact')->check()) {
auth()->guard('contact')->logout();
$request->session()->invalidate();
}
/*LOCALE SET */
if ($request->has('lang')) {
$locale = $request->input('lang');
App::setLocale($locale);
} elseif (auth()->guard('vendor')->user()) {
App::setLocale(auth()->guard('vendor')->user()->company->locale());
} elseif (auth()->user()) {
try{
App::setLocale(auth()->user()->company()->getLocale());
}
catch(\Exception $e){
}
} else {
App::setLocale(config('ninja.i18n.locale'));
}
return $next($request);
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\Account;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Account\BlackListRule;
use App\Http\ValidationRules\Account\EmailBlackListRule;
use App\Http\ValidationRules\NewUniqueUserRule;
use App\Utils\Ninja;
class UpdateAccountRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return (auth()->user()->isAdmin() || auth()->user()->isOwner()) && (int)$this->account->id === auth()->user()->account_id;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'set_react_as_default_ap' => 'required|bail|bool'
];
}
/* Only allow single field to update account table */
protected function prepareForValidation()
{
$input = $this->all();
$cleaned_input = array_intersect_key( $input, array_flip(['set_react_as_default_ap']));
$this->replace($cleaned_input);
}
}

View File

@ -23,7 +23,7 @@ class ShowInvoiceRequest extends Request
*/ */
public function authorize() : bool public function authorize() : bool
{ {
return auth()->guard('contact')->user()->client_id === $this->invoice->client_id return (int)auth()->guard('contact')->user()->client_id === (int)$this->invoice->client_id
&& auth()->guard('contact')->user()->company->enabled_modules & PortalComposer::MODULE_INVOICES; && auth()->guard('contact')->user()->company->enabled_modules & PortalComposer::MODULE_INVOICES;
} }
} }

View File

@ -27,9 +27,8 @@ class CreatePaymentMethodRequest extends FormRequest
$available_methods[] = $method['gateway_type_id']; $available_methods[] = $method['gateway_type_id'];
}); });
if (in_array($this->query('method'), $available_methods)) { if (in_array($this->query('method'), $available_methods))
return true; return true;
}
return false; return false;
} }

View File

@ -19,7 +19,7 @@ class ShowQuoteRequest extends FormRequest
{ {
public function authorize() public function authorize()
{ {
return auth()->guard('contact')->user()->client->id === $this->quote->client_id return auth()->guard('contact')->user()->client->id === (int)$this->quote->client_id
&& auth()->guard('contact')->user()->company->enabled_modules & PortalComposer::MODULE_QUOTES; && auth()->guard('contact')->user()->company->enabled_modules & PortalComposer::MODULE_QUOTES;
} }

View File

@ -39,21 +39,28 @@ class Checkout3dsRequest extends FormRequest
public function getCompany() public function getCompany()
{ {
MultiDB::findAndSetDbByCompanyKey($this->company_key); MultiDB::findAndSetDbByCompanyKey($this->company_key);
return Company::where('company_key', $this->company_key)->first(); return Company::where('company_key', $this->company_key)->first();
} }
public function getCompanyGateway() public function getCompanyGateway()
{ {
MultiDB::findAndSetDbByCompanyKey($this->company_key);
return CompanyGateway::find($this->decodePrimaryKey($this->company_gateway_id)); return CompanyGateway::find($this->decodePrimaryKey($this->company_gateway_id));
} }
public function getPaymentHash() public function getPaymentHash()
{ {
MultiDB::findAndSetDbByCompanyKey($this->company_key);
return PaymentHash::where('hash', $this->hash)->first(); return PaymentHash::where('hash', $this->hash)->first();
} }
public function getClient() public function getClient()
{ {
return Client::find($this->getPaymentHash()->data->client_id); MultiDB::findAndSetDbByCompanyKey($this->company_key);
return Client::withTrashed()->find($this->getPaymentHash()->data->client_id);
} }
} }

View File

@ -38,7 +38,7 @@ class StorePurchaseOrderRequest extends Request
{ {
$rules = []; $rules = [];
$rules['vendor_id'] = 'required'; $rules['vendor_id'] = 'bail|required|exists:vendors,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['number'] = ['nullable', Rule::unique('purchase_orders')->where('company_id', auth()->user()->company()->id)]; $rules['number'] = ['nullable', Rule::unique('purchase_orders')->where('company_id', auth()->user()->company()->id)];
$rules['discount'] = 'sometimes|numeric'; $rules['discount'] = 'sometimes|numeric';

View File

@ -0,0 +1,31 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\VendorPortal\PurchaseOrders;
use App\Http\ViewComposers\PortalComposer;
use Illuminate\Foundation\Http\FormRequest;
class ProcessPurchaseOrdersInBulkRequest extends FormRequest
{
public function authorize()
{
return auth()->guard('vendor')->user()->vendor->company->enabled_modules & PortalComposer::MODULE_PURCHASE_ORDERS;
}
public function rules()
{
return [
'purchase_orders' => ['array'],
];
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\VendorPortal\PurchaseOrders;
use App\Http\Requests\Request;
use App\Http\ViewComposers\PortalComposer;
class ShowPurchaseOrderRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return (int)auth()->guard('vendor')->user()->vendor_id === (int)$this->purchase_order->vendor_id
&& auth()->guard('vendor')->user()->company->enabled_modules & PortalComposer::MODULE_PURCHASE_ORDERS;
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\VendorPortal\PurchaseOrders;
use App\Http\Requests\Request;
use App\Http\ViewComposers\PortalComposer;
class ShowPurchaseOrdersRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->guard('vendor')->user()->company->enabled_modules & PortalComposer::MODULE_PURCHASE_ORDERS;
}
}

View File

@ -70,7 +70,7 @@ class CreateEntityPdf implements ShouldQueue
* *
* @param $invitation * @param $invitation
*/ */
public function __construct($invitation, $disk = 'public') public function __construct($invitation, $disk = null)
{ {
$this->invitation = $invitation; $this->invitation = $invitation;
@ -99,7 +99,7 @@ class CreateEntityPdf implements ShouldQueue
$this->client = $invitation->contact->client; $this->client = $invitation->contact->client;
$this->client->load('company'); $this->client->load('company');
$this->disk = Ninja::isHosted() ? config('filesystems.default') : $disk; $this->disk = $disk ?? config('filesystems.default');
} }

View File

@ -47,7 +47,7 @@ class ClientLedgerBalanceUpdate implements ShouldQueue
*/ */
public function handle() :void public function handle() :void
{ {
nlog("Updating company ledger for client ". $this->client->id); // nlog("Updating company ledger for client ". $this->client->id);
MultiDB::setDb($this->company->db); MultiDB::setDb($this->company->db);

View File

@ -0,0 +1,102 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\PurchaseOrder;
use App\Events\PurchaseOrder\PurchaseOrderWasEmailed;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\Engine\PurchaseOrderEmailEngine;
use App\Mail\VendorTemplateEmail;
use App\Models\Company;
use App\Models\PurchaseOrder;
use App\Utils\Ninja;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
class PurchaseOrderEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public PurchaseOrder $purchase_order;
public Company $company;
public $template_data;
public $tries = 1;
public function __construct(PurchaseOrder $purchase_order, Company $company, $template_data = null)
{
$this->purchase_order = $purchase_order;
$this->company = $company;
$this->template_data = $template_data;
}
/**
* Execute the job.
*
*
* @return void
*/
public function handle()
{
MultiDB::setDb($this->company->db);
$this->purchase_order->last_sent_date = now();
$this->purchase_order->invitations->load('contact.vendor.country', 'purchase_order.vendor.country', 'purchase_order.company')->each(function ($invitation) {
/* Don't fire emails if the company is disabled */
if ($this->company->is_disabled)
return true;
/* Set DB */
MultiDB::setDB($this->company->db);
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($invitation->contact->preferredLocale());
$t->replace(Ninja::transformTranslations($this->company->settings));
/* Mark entity sent */
$invitation->purchase_order->service()->markSent()->save();
$email_builder = (new PurchaseOrderEmailEngine($invitation, 'purchase_order', $this->template_data))->build();
$nmo = new NinjaMailerObject;
$nmo->mailable = new VendorTemplateEmail($email_builder, $invitation->contact, $invitation);
$nmo->company = $this->company;
$nmo->settings = $this->company->settings;
$nmo->to_user = $invitation->contact;
$nmo->entity_string = 'purchase_order';
$nmo->invitation = $invitation;
$nmo->reminder_template = 'purchase_order';
$nmo->entity = $invitation->purchase_order;
NinjaMailerJob::dispatchNow($nmo);
});
if ($this->purchase_order->invitations->count() >= 1) {
event(new PurchaseOrderWasEmailed($this->purchase_order->invitations->first(), $this->purchase_order->invitations->first()->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
}
}
}

View File

@ -0,0 +1,126 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\PurchaseOrder;
use App\Jobs\Entity\CreateEntityPdf;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Util\UnlinkFile;
use App\Jobs\Vendor\CreatePurchaseOrderPdf;
use App\Libraries\MultiDB;
use App\Mail\DownloadInvoices;
use App\Mail\DownloadPurchaseOrders;
use App\Models\Company;
use App\Models\User;
use App\Utils\TempFile;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use ZipArchive;
class ZipPurchaseOrders implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $purchase_orders;
private $company;
private $user;
public $settings;
public $tries = 1;
/**
* @param $purchase_orders
* @param Company $company
* @param $email
* @deprecated confirm to be deleted
* Create a new job instance.
*
*/
public function __construct($purchase_orders, Company $company, User $user)
{
$this->purchase_orders = $purchase_orders;
$this->company = $company;
$this->user = $user;
$this->settings = $company->settings;
}
/**
* Execute the job.
*
* @return void
* @throws \ZipStream\Exception\FileNotFoundException
* @throws \ZipStream\Exception\FileNotReadableException
* @throws \ZipStream\Exception\OverflowException
*/
public function handle()
{
MultiDB::setDb($this->company->db);
# create new zip object
$zipFile = new \PhpZip\ZipFile();
$file_name = date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.invoices')).'.zip';
$invitation = $this->purchase_orders->first()->invitations->first();
$path = $this->purchase_orders->first()->vendor->purchase_order_filepath($invitation);
$this->purchase_orders->each(function ($purchase_order){
CreatePurchaseOrderPdf::dispatchNow($purchase_order->invitations()->first());
});
try{
foreach ($this->purchase_orders as $purchase_order) {
$file = $purchase_order->service()->getPurchaseOrderPdf();
$zip_file_name = basename($file);
$zipFile->addFromString($zip_file_name, Storage::get($file));
}
Storage::put($path.$file_name, $zipFile->outputAsString());
$nmo = new NinjaMailerObject;
$nmo->mailable = new DownloadPurchaseOrders(Storage::url($path.$file_name), $this->company);
$nmo->to_user = $this->user;
$nmo->settings = $this->settings;
$nmo->company = $this->company;
NinjaMailerJob::dispatch($nmo);
UnlinkFile::dispatch(config('filesystems.default'), $path.$file_name)->delay(now()->addHours(1));
}
catch(\PhpZip\Exception\ZipException $e){
nlog("could not make zip => ". $e->getMessage());
}
finally{
$zipFile->close();
}
}
}

View File

@ -70,7 +70,7 @@ class CreatePurchaseOrderPdf implements ShouldQueue
* *
* @param $invitation * @param $invitation
*/ */
public function __construct($invitation, $disk = 'public') public function __construct($invitation, $disk = null)
{ {
$this->invitation = $invitation; $this->invitation = $invitation;
$this->company = $invitation->company; $this->company = $invitation->company;
@ -83,7 +83,7 @@ class CreatePurchaseOrderPdf implements ShouldQueue
$this->vendor = $invitation->contact->vendor; $this->vendor = $invitation->contact->vendor;
$this->vendor->load('company'); $this->vendor->load('company');
$this->disk = Ninja::isHosted() ? config('filesystems.default') : $disk; $this->disk = $disk ?? config('filesystems.default');
} }

View File

@ -29,6 +29,8 @@ class OAuth
const SOCIAL_LINKEDIN = 4; const SOCIAL_LINKEDIN = 4;
const SOCIAL_TWITTER = 5; const SOCIAL_TWITTER = 5;
const SOCIAL_BITBUCKET = 6; const SOCIAL_BITBUCKET = 6;
const SOCIAL_MICROSOFT = 7;
const SOCIAL_APPLE = 8;
/** /**
* @param Socialite $user * @param Socialite $user
@ -38,8 +40,8 @@ class OAuth
{ {
/** 1. Ensure user arrives on the correct provider **/ /** 1. Ensure user arrives on the correct provider **/
$query = [ $query = [
'oauth_user_id' =>$socialite_user->getId(), 'oauth_user_id' => $socialite_user->getId(),
'oauth_provider_id'=>$provider, 'oauth_provider_id' => $provider,
]; ];
if ($user = MultiDB::hasUser($query)) { if ($user = MultiDB::hasUser($query)) {
@ -54,12 +56,12 @@ class OAuth
{ {
$name = trim($name); $name = trim($name);
$last_name = (strpos($name, ' ') === false) ? '' : preg_replace('#.*\s([\w-]*)$#', '$1', $name); $last_name = (strpos($name, ' ') === false) ? '' : preg_replace('#.*\s([\w-]*)$#', '$1', $name);
$first_name = trim(preg_replace('#'.preg_quote($last_name, '/').'#', '', $name)); $first_name = trim(preg_replace('#' . preg_quote($last_name, '/') . '#', '', $name));
return [$first_name, $last_name]; return [$first_name, $last_name];
} }
public static function providerToString(int $social_provider) : string public static function providerToString(int $social_provider): string
{ {
switch ($social_provider) { switch ($social_provider) {
case SOCIAL_GOOGLE: case SOCIAL_GOOGLE:
@ -74,10 +76,14 @@ class OAuth
return 'twitter'; return 'twitter';
case SOCIAL_BITBUCKET: case SOCIAL_BITBUCKET:
return 'bitbucket'; return 'bitbucket';
case SOCIAL_MICROSOFT:
return 'microsoft';
case SOCIAL_APPLE:
return 'apple';
} }
} }
public static function providerToInt(string $social_provider) : int public static function providerToInt(string $social_provider): int
{ {
switch ($social_provider) { switch ($social_provider) {
case 'google': case 'google':
@ -92,6 +98,10 @@ class OAuth
return SOCIAL_TWITTER; return SOCIAL_TWITTER;
case 'bitbucket': case 'bitbucket':
return SOCIAL_BITBUCKET; return SOCIAL_BITBUCKET;
case 'microsoft':
return SOCIAL_MICROSOFT;
case 'apple':
return SOCIAL_APPLE;
} }
} }
@ -103,7 +113,6 @@ class OAuth
$this->provider_id = self::SOCIAL_GOOGLE; $this->provider_id = self::SOCIAL_GOOGLE;
return $this; return $this;
default: default:
return null; return null;
break; break;

View File

@ -49,7 +49,9 @@ class InvitationViewedListener implements ShouldQueue
if($entity_name == 'recurringInvoice') if($entity_name == 'recurringInvoice')
return; return;
elseif($entity_name == 'purchaseOrder')
$entity_name = 'purchase_order';
$nmo = new NinjaMailerObject; $nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer( (new EntityViewedObject($invitation, $entity_name))->build() ); $nmo->mailable = new NinjaMailer( (new EntityViewedObject($invitation, $entity_name))->build() );
$nmo->company = $invitation->company; $nmo->company = $invitation->company;
@ -60,6 +62,8 @@ class InvitationViewedListener implements ShouldQueue
$entity_viewed = "{$entity_name}_viewed"; $entity_viewed = "{$entity_name}_viewed";
$entity_viewed_all = "{$entity_name}_viewed_all"; $entity_viewed_all = "{$entity_name}_viewed_all";
$methods = $this->findUserNotificationTypes($invitation, $company_user, $entity_name, ['all_notifications', $entity_viewed, $entity_viewed_all]); $methods = $this->findUserNotificationTypes($invitation, $company_user, $entity_name, ['all_notifications', $entity_viewed, $entity_viewed_all]);
if (($key = array_search('mail', $methods)) !== false) { if (($key = array_search('mail', $methods)) !== false) {

View File

@ -0,0 +1,61 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Listeners\PurchaseOrder;
use App\Libraries\MultiDB;
use App\Models\Activity;
use App\Repositories\ActivityRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use stdClass;
class PurchaseOrderAcceptedActivity implements ShouldQueue
{
protected $activity_repo;
public $delay = 5;
/**
* Create the event listener.
*
* @param ActivityRepository $activity_repo
*/
public function __construct(ActivityRepository $activity_repo)
{
$this->activity_repo = $activity_repo;
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
MultiDB::setDb($event->company->db);
$fields = new stdClass;
$user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->purchase_order->user_id;
$event->purchase_order->service()->markSent()->save();
$fields->user_id = $user_id;
$fields->company_id = $event->purchase_order->company_id;
$fields->activity_type_id = Activity::ACCEPT_PURCHASE_ORDER;
$fields->vendor_id = $event->purchase_order->vendor_id;
$fields->vendor_contact_id = $event->contact->id;
$fields->purchase_order_id = $event->purchase_order->id;
$this->activity_repo->save($fields, $event->purchase_order, $event->event_vars);
}
}

View File

@ -0,0 +1,79 @@
<?php
/**
* Quote Ninja (https://quoteninja.com).
*
* @link https://github.com/quoteninja/quoteninja source repository
*
* @copyright Copyright (c) 2022. Quote Ninja LLC (https://quoteninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Listeners\PurchaseOrder;
use App\Jobs\Mail\NinjaMailer;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\Admin\EntityCreatedObject;
use App\Mail\Admin\PurchaseOrderAcceptedObject;
use App\Notifications\Admin\EntitySentNotification;
use App\Utils\Traits\Notifications\UserNotifies;
use Illuminate\Contracts\Queue\ShouldQueue;
class PurchaseOrderAcceptedNotification implements ShouldQueue
{
use UserNotifies;
public $delay = 5;
public function __construct()
{
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
MultiDB::setDb($event->company->db);
$first_notification_sent = true;
$purchase_order = $event->purchase_order;
$nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer( (new PurchaseOrderAcceptedObject($purchase_order, $event->company))->build() );
$nmo->company = $event->company;
$nmo->settings = $event->company->settings;
/* We loop through each user and determine whether they need to be notified */
foreach ($event->company->company_users as $company_user) {
/* The User */
$user = $company_user->user;
if(!$user)
continue;
/* Returns an array of notification methods */
$methods = $this->findUserNotificationTypes($purchase_order->invitations()->first(), $company_user, 'purchase_order', ['all_notifications', 'purchase_order_accepted', 'purchase_order_accepted_all']);
/* If one of the methods is email then we fire the EntitySentMailer */
if (($key = array_search('mail', $methods)) !== false) {
unset($methods[$key]);
$nmo->to_user = $user;
NinjaMailerJob::dispatch($nmo);
/* This prevents more than one notification being sent */
$first_notification_sent = false;
}
}
}
}

View File

@ -65,7 +65,12 @@ class EntityViewedObject
private function getAmount() private function getAmount()
{ {
return Number::formatMoney($this->entity->amount, $this->entity->client); if($this->entity->client)
$currency_entity = $this->entity->client;
else
$currency_entity = $this->company;
return Number::formatMoney($this->entity->amount, $currency_entity);
} }
private function getSubject() private function getSubject()
@ -82,7 +87,10 @@ class EntityViewedObject
private function getData() private function getData()
{ {
$settings = $this->entity->client->getMergedSettings(); if($this->entity->client)
$settings = $this->entity->client->getMergedSettings();
else
$settings = $this->company->settings;
$data = [ $data = [
'title' => $this->getSubject(), 'title' => $this->getSubject(),

View File

@ -0,0 +1,103 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Mail\Admin;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\PurchaseOrder;
use App\Utils\Ninja;
use App\Utils\Number;
use Illuminate\Support\Facades\App;
use stdClass;
class PurchaseOrderAcceptedObject
{
public $purchase_order;
public $company;
public $settings;
public function __construct(PurchaseOrder $purchase_order, Company $company)
{
$this->purchase_order = $purchase_order;
$this->company = $company;
}
public function build()
{
MultiDB::setDb($this->company->db);
if(!$this->purchase_order)
return;
App::forgetInstance('translator');
/* Init a new copy of the translator*/
$t = app('translator');
/* Set the locale*/
App::setLocale($this->company->getLocale());
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->company->settings));
$mail_obj = new stdClass;
$mail_obj->amount = $this->getAmount();
$mail_obj->subject = $this->getSubject();
$mail_obj->data = $this->getData();
$mail_obj->markdown = 'email.admin.generic';
$mail_obj->tag = $this->company->company_key;
return $mail_obj;
}
private function getAmount()
{
return Number::formatMoney($this->purchase_order->amount, $this->company);
}
private function getSubject()
{
return
ctrans(
"texts.notification_purchase_order_accepted_subject",
[
'vendor' => $this->purchase_order->vendor->present()->name(),
'purchase_order' => $this->purchase_order->number,
]
);
}
private function getData()
{
$settings = $this->company->settings;
$data = [
'title' => $this->getSubject(),
'message' => ctrans(
"texts.notification_purchase_order_accepted",
[
'amount' => $this->getAmount(),
'vendor' => $this->purchase_order->vendor->present()->name(),
'purchase_order' => $this->purchase_order->number,
]
),
'url' => $this->purchase_order->invitations->first()->getAdminLink(),
'button' => ctrans("texts.view_purchase_order"),
'signature' => $settings->email_signature,
'logo' => $this->company->present()->logo(),
'settings' => $settings,
'whitelabel' => $this->company->account->isPaid() ? true : false,
];
return $data;
}
}

View File

@ -0,0 +1,56 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Mail;
use App\Models\Company;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
class DownloadPurchaseOrders extends Mailable
{
// use Queueable, SerializesModels;
public $file_path;
public $company;
public function __construct($file_path, Company $company)
{
$this->file_path = $file_path;
$this->company = $company;
}
/**
* Build the message.
*/
public function build()
{
App::setLocale($this->company->getLocale());
return $this->from(config('mail.from.address'), config('mail.from.name'))
->subject(ctrans('texts.download_files'))
->text('email.admin.download_invoices_text', [
'url' => $this->file_path,
])
->view('email.admin.download_purchase_orders', [
'url' => $this->file_path,
'logo' => $this->company->present()->logo,
'whitelabel' => $this->company->account->isPaid() ? true : false,
'settings' => $this->company->settings,
'greeting' => $this->company->present()->name(),
]);
}
}

View File

@ -0,0 +1,207 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Mail\Engine;
use App\DataMapper\EmailTemplateDefaults;
use App\Jobs\Entity\CreateEntityPdf;
use App\Models\Account;
use App\Models\Expense;
use App\Models\PurchaseOrder;
use App\Models\Task;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\Number;
use App\Utils\Traits\MakesHash;
use App\Utils\VendorHtmlEngine;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Lang;
class PurchaseOrderEmailEngine extends BaseEmailEngine
{
use MakesHash;
public $invitation;
public Vendor $vendor;
public PurchaseOrder $purchase_order;
public $contact;
public $reminder_template;
public $template_data;
public function __construct($invitation, $reminder_template, $template_data)
{
$this->invitation = $invitation;
$this->reminder_template = $reminder_template; //'purchase_order'
$this->vendor = $invitation->contact->vendor;
$this->purchase_order = $invitation->purchase_order;
$this->contact = $invitation->contact;
$this->template_data = $template_data;
}
public function build()
{
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->vendor->company->settings));
if (is_array($this->template_data) && array_key_exists('body', $this->template_data) && strlen($this->template_data['body']) > 0) {
$body_template = $this->template_data['body'];
} elseif (strlen($this->vendor->getSetting('email_template_'.$this->reminder_template)) > 0) {
$body_template = $this->vendor->getSetting('email_template_'.$this->reminder_template);
} else {
$body_template = EmailTemplateDefaults::getDefaultTemplate('email_template_'.$this->reminder_template, $this->vendor->company->locale());
}
/* Use default translations if a custom message has not been set*/
if (iconv_strlen($body_template) == 0) {
$body_template = trans(
'texts.invoice_message',
[
'invoice' => $this->purchase_order->number,
'company' => $this->purchase_order->company->present()->name(),
'amount' => Number::formatMoney($this->purchase_order->balance, $this->vendor),
],
null,
$this->vendor->company->locale()
);
$body_template .= '<div class="center">$view_button</div>';
}
$text_body = trans(
'texts.purchase_order_message',
[
'purchase_order' => $this->purchase_order->number,
'company' => $this->purchase_order->company->present()->name(),
'amount' => Number::formatMoney($this->purchase_order->balance, $this->vendor),
],
null,
$this->vendor->company->locale()
) . "\n\n" . $this->invitation->getLink();
if (is_array($this->template_data) && array_key_exists('subject', $this->template_data) && strlen($this->template_data['subject']) > 0) {
$subject_template = $this->template_data['subject'];
} elseif (strlen($this->vendor->getSetting('email_subject_'.$this->reminder_template)) > 0) {
$subject_template = $this->vendor->getSetting('email_subject_'.$this->reminder_template);
} else {
$subject_template = EmailTemplateDefaults::getDefaultTemplate('email_subject_'.$this->reminder_template, $this->vendor->company->locale());
}
if (iconv_strlen($subject_template) == 0) {
$subject_template = trans(
'texts.purchase_order_subject',
[
'number' => $this->purchase_order->number,
'account' => $this->purchase_order->company->present()->name(),
],
null,
$this->vendor->company->locale()
);
}
$this->setTemplate($this->vendor->getSetting('email_style'))
->setContact($this->contact)
->setVariables((new VendorHtmlEngine($this->invitation))->makeValues())//move make values into the htmlengine
->setSubject($subject_template)
->setBody($body_template)
->setFooter("<a href='{$this->invitation->getLink()}'>".ctrans('texts.view_purchase_order').'</a>')
->setViewLink($this->invitation->getLink())
->setViewText(ctrans('texts.view_purchase_order'))
->setInvitation($this->invitation)
->setTextBody($text_body);
if ($this->vendor->getSetting('pdf_email_attachment') !== false && $this->purchase_order->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
if(Ninja::isHosted())
$this->setAttachments([$this->purchase_order->pdf_file_path($this->invitation, 'url', true)]);
else
$this->setAttachments([$this->purchase_order->pdf_file_path($this->invitation)]);
}
//attach third party documents
if($this->vendor->getSetting('document_email_attachment') !== false && $this->purchase_order->company->account->hasFeature(Account::FEATURE_DOCUMENTS)){
// Storage::url
foreach($this->purchase_order->documents as $document){
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => $document->type]]);
}
foreach($this->purchase_order->company->documents as $document){
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => $document->type]]);
}
// $line_items = $this->purchase_order->line_items;
// foreach($line_items as $item)
// {
// $expense_ids = [];
// if(property_exists($item, 'expense_id'))
// {
// $expense_ids[] = $item->expense_id;
// }
// if(count($expense_ids) > 0){
// $expenses = Expense::whereIn('id', $this->transformKeys($expense_ids))
// ->where('invoice_documents', 1)
// ->cursor()
// ->each(function ($expense){
// foreach($expense->documents as $document)
// {
// $this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => $document->type]]);
// }
// });
// }
// $task_ids = [];
// if(property_exists($item, 'task_id'))
// {
// $task_ids[] = $item->task_id;
// }
// if(count($task_ids) > 0 && $this->purchase_order->company->purchase_order_task_documents){
// $tasks = Task::whereIn('id', $this->transformKeys($task_ids))
// ->cursor()
// ->each(function ($task){
// foreach($task->documents as $document)
// {
// $this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => $document->type]]);
// }
// });
// }
// }
}
return $this;
}
}

View File

@ -0,0 +1,132 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Mail;
use App\Jobs\Invoice\CreateUbl;
use App\Models\Account;
use App\Models\Client;
use App\Models\User;
use App\Models\VendorContact;
use App\Services\PdfMaker\Designs\Utilities\DesignHelpers;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\TemplateEngine;
use App\Utils\VendorHtmlEngine;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class VendorTemplateEmail extends Mailable
{
private $build_email;
private $vendor;
private $contact;
private $company;
private $invitation;
public function __construct($build_email, VendorContact $contact, $invitation = null)
{
$this->build_email = $build_email;
$this->contact = $contact;
$this->vendor = $contact->vendor;
$this->company = $contact->company;
$this->invitation = $invitation;
}
public function build()
{
$template_name = 'email.template.'.$this->build_email->getTemplate();
if ($this->build_email->getTemplate() == 'light' || $this->build_email->getTemplate() == 'dark') {
$template_name = 'email.template.client';
}
if($this->build_email->getTemplate() == 'custom') {
$this->build_email->setBody(str_replace('$body', $this->build_email->getBody(), $this->client->getSetting('email_style_custom')));
}
$settings = $this->company->settings;
if ($this->build_email->getTemplate() !== 'custom') {
$this->build_email->setBody(
DesignHelpers::parseMarkdownToHtml($this->build_email->getBody())
);
}
if($this->invitation)
{
$html_variables = (new VendorHtmlEngine($this->invitation))->makeValues();
$signature = str_replace(array_keys($html_variables), array_values($html_variables), $settings->email_signature);
}
else
$signature = $settings->email_signature;
if(property_exists($settings, 'email_from_name') && strlen($settings->email_from_name) > 1)
$email_from_name = $settings->email_from_name;
else
$email_from_name = $this->company->present()->name();
$this->from(config('mail.from.address'), $email_from_name);
if (strlen($settings->bcc_email) > 1)
$this->bcc(explode(",",str_replace(" ", "", $settings->bcc_email)));//remove whitespace if any has been inserted.
$this->subject($this->build_email->getSubject())
->text('email.template.text', [
'text_body' => $this->build_email->getTextBody(),
'whitelabel' => $this->vendor->user->account->isPaid() ? true : false,
'settings' => $settings,
])
->view($template_name, [
'greeting' => ctrans('texts.email_salutation', ['name' => $this->contact->present()->name()]),
'body' => $this->build_email->getBody(),
'footer' => $this->build_email->getFooter(),
'view_link' => $this->build_email->getViewLink(),
'view_text' => $this->build_email->getViewText(),
'title' => '',
'signature' => $signature,
'settings' => $settings,
'company' => $this->company,
'whitelabel' => $this->vendor->user->account->isPaid() ? true : false,
'logo' => $this->company->present()->logo($settings),
])
->withSwiftMessage(function ($message) {
$message->getHeaders()->addTextHeader('Tag', $this->company->company_key);
$message->invitation = $this->invitation;
});
/*In the hosted platform we need to slow things down a little for Storage to catch up.*/
if(Ninja::isHosted())
sleep(1);
foreach ($this->build_email->getAttachments() as $file) {
if(is_string($file))
$this->attach($file);
elseif(is_array($file))
$this->attach($file['path'], ['as' => $file['name'], 'mime' => $file['mime']]);
}
return $this;
}
}

View File

@ -11,6 +11,7 @@
namespace App\Models; namespace App\Models;
use App\Exceptions\ModelNotFoundException;
use App\Jobs\Mail\NinjaMailerJob; use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject; use App\Jobs\Mail\NinjaMailerObject;
use App\Mail\Ninja\EmailQuotaExceeded; use App\Mail\Ninja\EmailQuotaExceeded;
@ -57,6 +58,7 @@ class Account extends BaseModel
'utm_content', 'utm_content',
'user_agent', 'user_agent',
'platform', 'platform',
'set_react_as_default_ap',
]; ];
/** /**
@ -74,7 +76,8 @@ class Account extends BaseModel
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',
'onboarding' => 'object' 'onboarding' => 'object',
'set_react_as_default_ap' => 'bool'
]; ];
const PLAN_FREE = 'free'; const PLAN_FREE = 'free';
@ -87,6 +90,7 @@ class Account extends BaseModel
const FEATURE_TASKS = 'tasks'; const FEATURE_TASKS = 'tasks';
const FEATURE_EXPENSES = 'expenses'; const FEATURE_EXPENSES = 'expenses';
const FEATURE_QUOTES = 'quotes'; const FEATURE_QUOTES = 'quotes';
const FEATURE_PURCHASE_ORDERS = 'purchase_orders';
const FEATURE_CUSTOMIZE_INVOICE_DESIGN = 'custom_designs'; const FEATURE_CUSTOMIZE_INVOICE_DESIGN = 'custom_designs';
const FEATURE_DIFFERENT_DESIGNS = 'different_designs'; const FEATURE_DIFFERENT_DESIGNS = 'different_designs';
const FEATURE_EMAIL_TEMPLATES_REMINDERS = 'template_reminders'; const FEATURE_EMAIL_TEMPLATES_REMINDERS = 'template_reminders';
@ -162,6 +166,7 @@ class Account extends BaseModel
case self::FEATURE_TASKS: case self::FEATURE_TASKS:
case self::FEATURE_EXPENSES: case self::FEATURE_EXPENSES:
case self::FEATURE_QUOTES: case self::FEATURE_QUOTES:
case self::FEATURE_PURCHASE_ORDERS:
return true; return true;
case self::FEATURE_CUSTOMIZE_INVOICE_DESIGN: case self::FEATURE_CUSTOMIZE_INVOICE_DESIGN:
@ -468,4 +473,14 @@ class Account extends BaseModel
} }
public function resolveRouteBinding($value, $field = null)
{
if (is_numeric($value)) {
throw new ModelNotFoundException("Record with value {$value} not found");
}
return $this
->where('id', $this->decodePrimaryKey($value))->firstOrFail();
}
} }

View File

@ -116,7 +116,8 @@ class Activity extends StaticModel
const RESTORE_PURCHASE_ORDER = 134; const RESTORE_PURCHASE_ORDER = 134;
const EMAIL_PURCHASE_ORDER = 135; const EMAIL_PURCHASE_ORDER = 135;
const VIEW_PURCHASE_ORDER = 136; const VIEW_PURCHASE_ORDER = 136;
const ACCEPT_PURCHASE_ORDER = 137;
protected $casts = [ protected $casts = [
'is_system' => 'boolean', 'is_system' => 'boolean',
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',

View File

@ -486,7 +486,7 @@ class Client extends BaseModel implements HasLocalePreference
} }
if($this->currency()->code == 'EUR' && in_array(GatewayType::BANK_TRANSFER, array_column($pms, 'gateway_type_id'))){ if($this->currency()->code == 'EUR' && (in_array(GatewayType::BANK_TRANSFER, array_column($pms, 'gateway_type_id')) || in_array(GatewayType::SEPA, array_column($pms, 'gateway_type_id'))) ){
foreach($pms as $pm){ foreach($pms as $pm){
@ -501,18 +501,6 @@ class Client extends BaseModel implements HasLocalePreference
} }
// if ($this->currency()->code == 'EUR' && in_array(GatewayType::SEPA, array_column($pms, 'gateway_type_id'))) {
// foreach ($pms as $pm) {
// if ($pm['gateway_type_id'] == GatewayType::SEPA) {
// $cg = CompanyGateway::find($pm['company_gateway_id']);
// if ($cg && $cg->fees_and_limits->{GatewayType::SEPA}->is_enabled) {
// return $cg;
// }
// }
// }
// }
if ($this->country && $this->country->iso_3166_3 == 'GBR' && in_array(GatewayType::DIRECT_DEBIT, array_column($pms, 'gateway_type_id'))) { if ($this->country && $this->country->iso_3166_3 == 'GBR' && in_array(GatewayType::DIRECT_DEBIT, array_column($pms, 'gateway_type_id'))) {
foreach ($pms as $pm) { foreach ($pms as $pm) {
if ($pm['gateway_type_id'] == GatewayType::DIRECT_DEBIT) { if ($pm['gateway_type_id'] == GatewayType::DIRECT_DEBIT) {

View File

@ -14,6 +14,7 @@ namespace App\Models;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use App\Models\Language; use App\Models\Language;
use App\Models\Presenters\CompanyPresenter; use App\Models\Presenters\CompanyPresenter;
use App\Models\PurchaseOrder;
use App\Models\User; use App\Models\User;
use App\Services\Notification\NotificationService; use App\Services\Notification\NotificationService;
use App\Utils\Ninja; use App\Utils\Ninja;
@ -192,6 +193,11 @@ class Company extends BaseModel
return $this->hasMany(Subscription::class)->withTrashed(); return $this->hasMany(Subscription::class)->withTrashed();
} }
public function purchase_orders()
{
return $this->hasMany(PurchaseOrder::class)->withTrashed();
}
public function task_statuses() public function task_statuses()
{ {
return $this->hasMany(TaskStatus::class)->withTrashed(); return $this->hasMany(TaskStatus::class)->withTrashed();

View File

@ -346,7 +346,7 @@ class Invoice extends BaseModel
return '<h5><span class="badge badge-danger">'.ctrans('texts.overdue').'</span></h5>'; return '<h5><span class="badge badge-danger">'.ctrans('texts.overdue').'</span></h5>';
break; break;
case self::STATUS_UNPAID: case self::STATUS_UNPAID:
return '<h5><span class="badge badge-warning">'.ctrans('texts.unpaid').'</span></h5>'; return '<h5><span class="badge badge-warning text-white">'.ctrans('texts.unpaid').'</span></h5>';
break; break;
case self::STATUS_REVERSED: case self::STATUS_REVERSED:
return '<h5><span class="badge badge-info">'.ctrans('texts.reversed').'</span></h5>'; return '<h5><span class="badge badge-info">'.ctrans('texts.reversed').'</span></h5>';

View File

@ -212,7 +212,7 @@ class Payment extends BaseModel
return '<h6><span class="badge badge-secondary">'.ctrans('texts.payment_status_1').'</span></h6>'; return '<h6><span class="badge badge-secondary">'.ctrans('texts.payment_status_1').'</span></h6>';
break; break;
case self::STATUS_CANCELLED: case self::STATUS_CANCELLED:
return '<h6><span class="badge badge-warning">'.ctrans('texts.payment_status_2').'</span></h6>'; return '<h6><span class="badge badge-warning text-white">'.ctrans('texts.payment_status_2').'</span></h6>';
break; break;
case self::STATUS_FAILED: case self::STATUS_FAILED:
return '<h6><span class="badge badge-danger">'.ctrans('texts.payment_status_3').'</span></h6>'; return '<h6><span class="badge badge-danger">'.ctrans('texts.payment_status_3').'</span></h6>';

View File

@ -18,6 +18,7 @@ use App\Jobs\Entity\CreateEntityPdf;
use App\Jobs\Vendor\CreatePurchaseOrderPdf; use App\Jobs\Vendor\CreatePurchaseOrderPdf;
use App\Services\PurchaseOrder\PurchaseOrderService; use App\Services\PurchaseOrder\PurchaseOrderService;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@ -26,6 +27,7 @@ class PurchaseOrder extends BaseModel
{ {
use Filterable; use Filterable;
use SoftDeletes; use SoftDeletes;
use MakesDates;
protected $fillable = [ protected $fillable = [
'number', 'number',
@ -99,8 +101,51 @@ class PurchaseOrder extends BaseModel
const STATUS_DRAFT = 1; const STATUS_DRAFT = 1;
const STATUS_SENT = 2; const STATUS_SENT = 2;
const STATUS_PARTIAL = 3; const STATUS_ACCEPTED = 3;
const STATUS_APPLIED = 4; const STATUS_CANCELLED = 4;
public static function stringStatus(int $status)
{
switch ($status) {
case self::STATUS_DRAFT:
return ctrans('texts.draft');
break;
case self::STATUS_SENT:
return ctrans('texts.sent');
break;
case self::STATUS_ACCEPTED:
return ctrans('texts.accepted');
break;
case self::STATUS_CANCELLED:
return ctrans('texts.cancelled');
break;
// code...
break;
}
}
public static function badgeForStatus(int $status)
{
switch ($status) {
case self::STATUS_DRAFT:
return '<h5><span class="badge badge-light">'.ctrans('texts.draft').'</span></h5>';
break;
case self::STATUS_SENT:
return '<h5><span class="badge badge-primary">'.ctrans('texts.sent').'</span></h5>';
break;
case self::STATUS_ACCEPTED:
return '<h5><span class="badge badge-primary">'.ctrans('texts.accepted').'</span></h5>';
break;
case self::STATUS_CANCELLED:
return '<h5><span class="badge badge-secondary">'.ctrans('texts.cancelled').'</span></h5>';
break;
default:
// code...
break;
}
}
public function assigned_user() public function assigned_user()
{ {

View File

@ -1,4 +1,13 @@
<?php <?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Models; namespace App\Models;
@ -9,6 +18,7 @@ use App\Utils\Traits\Inviteable;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class PurchaseOrderInvitation extends BaseModel class PurchaseOrderInvitation extends BaseModel
{ {
@ -104,4 +114,37 @@ class PurchaseOrderInvitation extends BaseModel
} }
public function getLink() :string
{
$entity_type = Str::snake(class_basename($this->entityType()));
if(Ninja::isHosted()){
$domain = $this->company->domain();
}
else
$domain = config('ninja.app_url');
switch ($this->company->portal_mode) {
case 'subdomain':
return $domain.'/vendor/'.$entity_type.'/'.$this->key;
break;
case 'iframe':
return $domain.'/vendor/'.$entity_type.'/'.$this->key;
break;
case 'domain':
return $domain.'/vendor/'.$entity_type.'/'.$this->key;
break;
default:
return '';
break;
}
}
public function getAdminLink() :string
{
return $this->getLink().'?silent=true';
}
} }

View File

@ -73,6 +73,24 @@ class VendorContact extends Authenticatable implements HasLocalePreference
'vendor_id', 'vendor_id',
]; ];
public function avatar()
{
if ($this->avatar) {
return $this->avatar;
}
return asset('images/svg/user.svg');
}
public function setAvatarAttribute($value)
{
if (! filter_var($value, FILTER_VALIDATE_URL) && $value) {
$this->attributes['avatar'] = url('/').$value;
} else {
$this->attributes['avatar'] = $value;
}
}
public function getEntityType() public function getEntityType()
{ {
return self::class; return self::class;
@ -110,7 +128,7 @@ class VendorContact extends Authenticatable implements HasLocalePreference
public function sendPasswordResetNotification($token) public function sendPasswordResetNotification($token)
{ {
$this->notify(new ClientContactResetPassword($token)); // $this->notify(new ClientContactResetPassword($token));
} }
public function preferredLocale() public function preferredLocale()
@ -118,12 +136,9 @@ class VendorContact extends Authenticatable implements HasLocalePreference
$languages = Cache::get('languages'); $languages = Cache::get('languages');
return $languages->filter(function ($item) { return $languages->filter(function ($item) {
return $item->id == $this->client->getSetting('language_id'); return $item->id == $this->company->getSetting('language_id');
})->first()->locale; })->first()->locale;
//$lang = Language::find($this->client->getSetting('language_id'));
//return $lang->locale;
} }
/** /**

View File

@ -407,12 +407,7 @@ class BaseDriver extends AbstractPaymentDriver
$this->unWindGatewayFees($this->payment_hash); $this->unWindGatewayFees($this->payment_hash);
} }
if ($e instanceof CheckoutHttpException) { $error = $e->getMessage();
$error = $e->getBody();
} else if ($e instanceof Exception) {
$error = $e->getMessage();
} else
$error = $e->getMessage();
if(!$this->payment_hash) if(!$this->payment_hash)
throw new PaymentFailed($error, $e->getCode()); throw new PaymentFailed($error, $e->getCode());

View File

@ -14,18 +14,24 @@ namespace App\PaymentDrivers\CheckoutCom;
use App\Exceptions\PaymentFailed; use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use Illuminate\Http\Request;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Models\Payment;
use App\PaymentDrivers\CheckoutComPaymentDriver; use App\PaymentDrivers\CheckoutComPaymentDriver;
use App\PaymentDrivers\Common\MethodInterface; use App\PaymentDrivers\Common\MethodInterface;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Checkout\CheckoutApiException;
use Checkout\CheckoutArgumentException;
use Checkout\CheckoutAuthorizationException;
use Checkout\Library\Exceptions\CheckoutHttpException; use Checkout\Library\Exceptions\CheckoutHttpException;
use Checkout\Models\Payments\IdSource; use Checkout\Models\Payments\IdSource;
use Checkout\Models\Payments\Payment; use Checkout\Payments\Four\Request\Source\RequestTokenSource;
use Checkout\Models\Payments\TokenSource; use Checkout\Payments\Source\RequestTokenSource as SourceRequestTokenSource;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\View\View; use Illuminate\View\View;
use Checkout\Payments\PaymentRequest as PaymentsPaymentRequest;
use Checkout\Payments\Four\Request\PaymentRequest;
class CreditCard implements MethodInterface class CreditCard implements MethodInterface
{ {
@ -57,6 +63,31 @@ class CreditCard implements MethodInterface
return render('gateways.checkout.credit_card.authorize', $data); return render('gateways.checkout.credit_card.authorize', $data);
} }
public function bootRequest($token)
{
if($this->checkout->is_four_api){
$token_source = new RequestTokenSource();
$token_source->token = $token;
$request = new PaymentRequest();
$request->source = $token_source;
}
else {
$token_source = new SourceRequestTokenSource();
$token_source->token = $token;
$request = new PaymentsPaymentRequest();
$request->source = $token_source;
}
return $request;
}
/** /**
* Handle authorization for credit card. * Handle authorization for credit card.
* *
@ -67,41 +98,54 @@ class CreditCard implements MethodInterface
{ {
$gateway_response = \json_decode($request->gateway_response); $gateway_response = \json_decode($request->gateway_response);
$method = new TokenSource( $customerRequest = $this->checkout->getCustomer();
$gateway_response->token $request = $this->bootRequest($gateway_response->token);
); $request->capture = false;
$request->reference = '$1 payment for authorization.';
$request->amount = 100;
$request->currency = $this->checkout->client->getCurrencyCode();
$request->customer = $customerRequest;
$payment = new Payment($method, 'USD');
$payment->amount = 100; // $1
$payment->reference = '$1 payment for authorization.';
$payment->capture = false;
try { try {
$response = $this->checkout->gateway->payments()->request($payment);
if ($response->approved && $response->status === 'Authorized') { $response = $this->checkout->gateway->getPaymentsClient()->requestPayment($request);
if ($response['approved'] && $response['status'] === 'Authorized') {
$payment_meta = new \stdClass; $payment_meta = new \stdClass;
$payment_meta->exp_month = (string) $response->source['expiry_month']; $payment_meta->exp_month = (string) $response['source']['expiry_month'];
$payment_meta->exp_year = (string) $response->source['expiry_year']; $payment_meta->exp_year = (string) $response['source']['expiry_year'];
$payment_meta->brand = (string) $response->source['scheme']; $payment_meta->brand = (string) $response['source']['scheme'];
$payment_meta->last4 = (string) $response->source['last4']; $payment_meta->last4 = (string) $response['source']['last4'];
$payment_meta->type = (int) GatewayType::CREDIT_CARD; $payment_meta->type = (int) GatewayType::CREDIT_CARD;
$data = [ $data = [
'payment_meta' => $payment_meta, 'payment_meta' => $payment_meta,
'token' => $response->source['id'], 'token' => $response['source']['id'],
'payment_method_id' => GatewayType::CREDIT_CARD, 'payment_method_id' => GatewayType::CREDIT_CARD,
]; ];
$payment_method = $this->checkout->storeGatewayToken($data); $payment_method = $this->checkout->storeGatewayToken($data,['gateway_customer_reference' => $customerRequest['id']]);
return redirect()->route('client.payment_methods.show', $payment_method->hashed_id); return redirect()->route('client.payment_methods.show', $payment_method->hashed_id);
} }
} catch (CheckoutHttpException $exception) {
throw new PaymentFailed(
$exception->getMessage() } catch (CheckoutApiException $e) {
); // API error
$request_id = $e->request_id;
$http_status_code = $e->http_status_code;
$error_details = $e->error_details;
throw new PaymentFailed($e->getMessage());
} catch (CheckoutArgumentException $e) {
// Bad arguments
throw new PaymentFailed($e->getMessage());
} catch (CheckoutAuthorizationException $e) {
// Bad Invalid authorization
throw new PaymentFailed($e->getMessage());
} }
} }
public function paymentView($data) public function paymentView($data)
@ -145,89 +189,102 @@ class CreditCard implements MethodInterface
{ {
$cgt = ClientGatewayToken::query() $cgt = ClientGatewayToken::query()
->where('id', $this->decodePrimaryKey($request->input('token'))) ->where('id', $this->decodePrimaryKey($request->input('token')))
->where('company_id', auth()->guard('contact')->user()->client->company->id) ->where('company_id', auth()->guard('contact')->user()->client->company_id)
->first(); ->first();
if (!$cgt) { if (!$cgt) {
throw new PaymentFailed(ctrans('texts.payment_token_not_found'), 401); throw new PaymentFailed(ctrans('texts.payment_token_not_found'), 401);
} }
$method = new IdSource($cgt->token); $paymentRequest = $this->checkout->bootTokenRequest($cgt->token);
return $this->completePayment($method, $request); return $this->completePayment($paymentRequest, $request);
} }
private function attemptPaymentUsingCreditCard(PaymentResponseRequest $request) private function attemptPaymentUsingCreditCard(PaymentResponseRequest $request)
{ {
$checkout_response = $this->checkout->payment_hash->data->server_response; $checkout_response = $this->checkout->payment_hash->data->server_response;
$method = new TokenSource( $paymentRequest = $this->bootRequest($checkout_response->token);
$checkout_response->token
);
return $this->completePayment($method, $request); return $this->completePayment($paymentRequest, $request);
} }
private function completePayment($method, PaymentResponseRequest $request) private function completePayment($paymentRequest, PaymentResponseRequest $request)
{ {
$payment = new Payment($method, $this->checkout->payment_hash->data->currency); $paymentRequest->amount = $this->checkout->payment_hash->data->value;
$payment->amount = $this->checkout->payment_hash->data->value; $paymentRequest->reference = $this->checkout->getDescription();
$payment->reference = $this->checkout->getDescription(); $paymentRequest->customer = $this->checkout->getCustomer();
$payment->customer = [ $paymentRequest->metadata = ['udf1' => "Invoice Ninja"];
'name' => $this->checkout->client->present()->name() , $paymentRequest->currency = $this->checkout->client->getCurrencyCode();
'email' => $this->checkout->client->present()->email(),
];
$payment->metadata = [ $this->checkout->payment_hash->data = array_merge((array)$this->checkout->payment_hash->data, ['checkout_payment_ref' => $paymentRequest]);
'udf1' => "Invoice Ninja",
];
$this->checkout->payment_hash->data = array_merge((array)$this->checkout->payment_hash->data, ['checkout_payment_ref' => $payment]);
$this->checkout->payment_hash->save(); $this->checkout->payment_hash->save();
if ($this->checkout->client->currency()->code == 'EUR' || $this->checkout->company_gateway->getConfigField('threeds')) { if ($this->checkout->client->currency()->code == 'EUR' || $this->checkout->company_gateway->getConfigField('threeds')) {
$payment->{'3ds'} = ['enabled' => true];
$payment->{'success_url'} = route('checkout.3ds_redirect', [ $paymentRequest->{'3ds'} = ['enabled' => true];
$paymentRequest->{'success_url'} = route('checkout.3ds_redirect', [
'company_key' => $this->checkout->client->company->company_key, 'company_key' => $this->checkout->client->company->company_key,
'company_gateway_id' => $this->checkout->company_gateway->hashed_id, 'company_gateway_id' => $this->checkout->company_gateway->hashed_id,
'hash' => $this->checkout->payment_hash->hash, 'hash' => $this->checkout->payment_hash->hash,
]); ]);
$payment->{'failure_url'} = route('checkout.3ds_redirect', [ $paymentRequest->{'failure_url'} = route('checkout.3ds_redirect', [
'company_key' => $this->checkout->client->company->company_key, 'company_key' => $this->checkout->client->company->company_key,
'company_gateway_id' => $this->checkout->company_gateway->hashed_id, 'company_gateway_id' => $this->checkout->company_gateway->hashed_id,
'hash' => $this->checkout->payment_hash->hash, 'hash' => $this->checkout->payment_hash->hash,
]); ]);
} }
try { try {
$response = $this->checkout->gateway->payments()->request($payment); // $response = $this->checkout->gateway->payments()->request($payment);
if ($response->status == 'Authorized') { $response = $this->checkout->gateway->getPaymentsClient()->requestPayment($paymentRequest);
if ($response['status'] == 'Authorized') {
return $this->processSuccessfulPayment($response); return $this->processSuccessfulPayment($response);
} }
if ($response->status == 'Pending') { if ($response['status'] == 'Pending') {
$this->checkout->confirmGatewayFee(); $this->checkout->confirmGatewayFee();
return $this->processPendingPayment($response); return $this->processPendingPayment($response);
} }
if ($response->status == 'Declined') { if ($response['status'] == 'Declined') {
$this->checkout->unWindGatewayFees($this->checkout->payment_hash); $this->checkout->unWindGatewayFees($this->checkout->payment_hash);
// $this->checkout->sendFailureMail($response->response_summary);
//@todo - this will double up the checkout . com failed mails
// $this->checkout->clientPaymentFailureMailer($response->status);
return $this->processUnsuccessfulPayment($response); return $this->processUnsuccessfulPayment($response);
} }
} catch (CheckoutHttpException $e) { }
catch (CheckoutApiException $e) {
// API error
$request_id = $e->request_id;
$http_status_code = $e->http_status_code;
$error_details = $e->error_details;
$this->checkout->unWindGatewayFees($this->checkout->payment_hash); $this->checkout->unWindGatewayFees($this->checkout->payment_hash);
return $this->checkout->processInternallyFailedPayment($this->checkout, $e); return $this->checkout->processInternallyFailedPayment($this->checkout, $e);
} catch (CheckoutArgumentException $e) {
// Bad arguments
$this->checkout->unWindGatewayFees($this->checkout->payment_hash);
return $this->checkout->processInternallyFailedPayment($this->checkout, $e);
} catch (CheckoutAuthorizationException $e) {
// Bad Invalid authorization
$this->checkout->unWindGatewayFees($this->checkout->payment_hash);
return $this->checkout->processInternallyFailedPayment($this->checkout, $e);
} }
} }
} }

View File

@ -54,17 +54,18 @@ trait Utilities
return round($amount * 100); return round($amount * 100);
} }
private function processSuccessfulPayment(Payment $_payment) private function processSuccessfulPayment($_payment)
{ {
if ($this->getParent()->payment_hash->data->store_card) { if ($this->getParent()->payment_hash->data->store_card) {
$this->storeLocalPaymentMethod($_payment); $this->storeLocalPaymentMethod($_payment);
} }
$data = [ $data = [
'payment_method' => $_payment->source['id'], 'payment_method' => $_payment['source']['id'],
'payment_type' => 12, 'payment_type' => 12,
'amount' => $this->getParent()->payment_hash->data->raw_value, 'amount' => $this->getParent()->payment_hash->data->raw_value,
'transaction_reference' => $_payment->id, 'transaction_reference' => $_payment['id'],
'gateway_type_id' => GatewayType::CREDIT_CARD, 'gateway_type_id' => GatewayType::CREDIT_CARD,
]; ];
@ -82,15 +83,15 @@ trait Utilities
return redirect()->route('client.payments.show', ['payment' => $this->getParent()->encodePrimaryKey($payment->id)]); return redirect()->route('client.payments.show', ['payment' => $this->getParent()->encodePrimaryKey($payment->id)]);
} }
public function processUnsuccessfulPayment(Payment $_payment, $throw_exception = true) public function processUnsuccessfulPayment($_payment, $throw_exception = true)
{ {
$error_message = ''; $error_message = '';
if(property_exists($_payment, 'server_response')) if(array_key_exists('response_summary',$_payment))
$error_message = $_payment->response_summary; $error_message = $_payment['response_summary'];
elseif(property_exists($_payment, 'status')) elseif(array_key_exists('status',$_payment))
$error_message = $_payment->status; $error_message = $_payment['status'];
$this->getParent()->sendFailureMail($error_message); $this->getParent()->sendFailureMail($error_message);
@ -110,36 +111,37 @@ trait Utilities
if ($throw_exception) { if ($throw_exception) {
throw new PaymentFailed($_payment->status . " " . $error_message, $_payment->http_code); throw new PaymentFailed($_payment['status'] . " " . $error_message, 500);
} }
} }
private function processPendingPayment(Payment $_payment) private function processPendingPayment($_payment)
{ {
try { try {
return redirect($_payment->_links['redirect']['href']); return redirect($_payment['_links']['redirect']['href']);
} catch (Exception $e) { } catch (Exception $e) {
return $this->processInternallyFailedPayment($this->getParent(), $e); return $this->getParent()->processInternallyFailedPayment($this->getParent(), $e);
} }
} }
private function storeLocalPaymentMethod(Payment $response) private function storeLocalPaymentMethod($response)
{ {
try { try {
$payment_meta = new stdClass; $payment_meta = new stdClass;
$payment_meta->exp_month = (string) $response->source['expiry_month']; $payment_meta->exp_month = (string) $response['source']['expiry_month'];
$payment_meta->exp_year = (string) $response->source['expiry_year']; $payment_meta->exp_year = (string) $response['source']['expiry_year'];
$payment_meta->brand = (string) $response->source['scheme']; $payment_meta->brand = (string) $response['source']['scheme'];
$payment_meta->last4 = (string) $response->source['last4']; $payment_meta->last4 = (string) $response['source']['last4'];
$payment_meta->type = (int) GatewayType::CREDIT_CARD; $payment_meta->type = (int) GatewayType::CREDIT_CARD;
$data = [ $data = [
'payment_meta' => $payment_meta, 'payment_meta' => $payment_meta,
'token' => $response->source['id'], 'token' => $response['source']['id'],
'payment_method_id' => $this->getParent()->payment_hash->data->payment_method_id, 'payment_method_id' => $this->getParent()->payment_hash->data->payment_method_id,
]; ];
return $this->getParent()->storePaymentMethod($data); return $this->getParent()->storePaymentMethod($data, ['gateway_customer_reference' => $response['customer']['id']]);
} catch (Exception $e) { } catch (Exception $e) {
session()->flash('message', ctrans('texts.payment_method_saving_failed')); session()->flash('message', ctrans('texts.payment_method_saving_failed'));
} }

View File

@ -28,10 +28,22 @@ use App\PaymentDrivers\CheckoutCom\CreditCard;
use App\PaymentDrivers\CheckoutCom\Utilities; use App\PaymentDrivers\CheckoutCom\Utilities;
use App\Utils\Traits\SystemLogTrait; use App\Utils\Traits\SystemLogTrait;
use Checkout\CheckoutApi; use Checkout\CheckoutApi;
use Checkout\CheckoutApiException;
use Checkout\CheckoutArgumentException;
use Checkout\CheckoutAuthorizationException;
use Checkout\CheckoutDefaultSdk;
use Checkout\CheckoutFourSdk;
use Checkout\Environment;
use Checkout\Library\Exceptions\CheckoutHttpException; use Checkout\Library\Exceptions\CheckoutHttpException;
use Checkout\Models\Payments\IdSource; use Checkout\Models\Payments\IdSource;
use Checkout\Models\Payments\Refund; use Checkout\Models\Payments\Refund;
use Exception; use Exception;
use Checkout\Payments\Four\Request\PaymentRequest;
use Checkout\Payments\Four\Request\Source\RequestIdSource as SourceRequestIdSource;
use Checkout\Payments\PaymentRequest as PaymentsPaymentRequest;
use Checkout\Payments\Source\RequestIdSource;
use Checkout\Common\CustomerRequest;
use Checkout\Payments\RefundRequest;
class CheckoutComPaymentDriver extends BaseDriver class CheckoutComPaymentDriver extends BaseDriver
{ {
@ -52,6 +64,8 @@ class CheckoutComPaymentDriver extends BaseDriver
/* Authorise payment methods */ /* Authorise payment methods */
public $can_authorise_credit_card = true; public $can_authorise_credit_card = true;
public $is_four_api = false;
/** /**
* @var CheckoutApi; * @var CheckoutApi;
*/ */
@ -109,7 +123,22 @@ class CheckoutComPaymentDriver extends BaseDriver
'sandbox' => $this->company_gateway->getConfigField('testMode'), 'sandbox' => $this->company_gateway->getConfigField('testMode'),
]; ];
$this->gateway = new CheckoutApi($config['secret'], $config['sandbox'], $config['public']);
if(strlen($config['secret']) <= 38){
$this->is_four_api = true;
$builder = CheckoutFourSdk::staticKeys();
$builder->setPublicKey($config['public']); // optional, only required for operations related with tokens
$builder->setSecretKey($config['secret']);
$builder->setEnvironment($config['sandbox'] ? Environment::sandbox(): Environment::production());
$this->gateway = $builder->build();
}
else {
$builder = CheckoutDefaultSdk::staticKeys();
$builder->setPublicKey($config['public']); // optional, only required for operations related with tokens
$builder->setSecretKey($config['secret']);
$builder->setEnvironment($config['sandbox'] ? Environment::sandbox(): Environment::production());
$this->gateway = $builder->build();
}
return $this; return $this;
} }
@ -121,9 +150,6 @@ class CheckoutComPaymentDriver extends BaseDriver
*/ */
public function viewForType($gateway_type_id) public function viewForType($gateway_type_id)
{ {
// At the moment Checkout.com payment
// driver only supports payments using credit card.
return 'gateways.checkout.credit_card.pay'; return 'gateways.checkout.credit_card.pay';
} }
@ -211,22 +237,40 @@ class CheckoutComPaymentDriver extends BaseDriver
{ {
$this->init(); $this->init();
$checkout_payment = new Refund($payment->transaction_reference); $request = new RefundRequest();
$request->reference = "{$payment->transaction_reference} " . now();
$request->amount = $this->convertToCheckoutAmount($amount, $this->client->getCurrencyCode());
try { try {
$refund = $this->gateway->payments()->refund($checkout_payment); // or, refundPayment("payment_id") for a full refund
$checkout_payment = $this->gateway->payments()->details($refund->id); $response = $this->gateway->getPaymentsClient()->refundPayment($payment->transaction_reference, $request);
$response = ['refund_response' => $refund, 'checkout_payment_fetch' => $checkout_payment];
return [ return [
'transaction_reference' => $refund->action_id, 'transaction_reference' => $response['action_id'],
'transaction_response' => json_encode($response), 'transaction_response' => json_encode($response),
'success' => $checkout_payment->status == 'Refunded', 'success' => true,
'description' => $checkout_payment->status, 'description' => $response['reference'],
'code' => $checkout_payment->http_code, 'code' => 202,
]; ];
} catch (CheckoutHttpException $e) {
} catch (CheckoutApiException $e) {
// API error
$request_id = $e->request_id;
$http_status_code = $e->http_status_code;
$error_details = $e->error_details;
} catch (CheckoutArgumentException $e) {
// Bad arguments
return [
'transaction_reference' => null,
'transaction_response' => json_encode($e->getMessage()),
'success' => false,
'description' => $e->getMessage(),
'code' => $e->getCode(),
];
} catch (CheckoutAuthorizationException $e) {
// Bad Invalid authorization
return [ return [
'transaction_reference' => null, 'transaction_reference' => null,
'transaction_response' => json_encode($e->getMessage()), 'transaction_response' => json_encode($e->getMessage()),
@ -235,6 +279,49 @@ class CheckoutComPaymentDriver extends BaseDriver
'code' => $e->getCode(), 'code' => $e->getCode(),
]; ];
} }
}
public function getCustomer()
{
try{
$response = $this->gateway->getCustomersClient()->get($this->client->present()->email());
return $response;
}
catch(\Exception $e){
$request = new CustomerRequest();
$request->email = $this->client->present()->email();
$request->name = $this->client->present()->name();
return $request;
}
}
public function bootTokenRequest($token)
{
if($this->is_four_api){
$token_source = new SourceRequestIdSource();
$token_source->id = $token;
$request = new PaymentRequest();
$request->source = $token_source;
}
else {
$token_source = new RequestIdSource();
$token_source->id = $token;
$request = new PaymentsPaymentRequest();
$request->source = $token_source;
}
return $request;
} }
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
@ -244,27 +331,29 @@ class CheckoutComPaymentDriver extends BaseDriver
$this->init(); $this->init();
$method = new IdSource($cgt->token); $paymentRequest = $this->bootTokenRequest($cgt->token);
$paymentRequest->amount = $this->convertToCheckoutAmount($amount, $this->client->getCurrencyCode());
$payment = new \Checkout\Models\Payments\Payment($method, $this->client->getCurrencyCode()); $paymentRequest->reference = '#' . $invoice->number . ' - ' . now();
$payment->amount = $this->convertToCheckoutAmount($amount, $this->client->getCurrencyCode()); $paymentRequest->customer = $this->getCustomer();
$payment->reference = $invoice->number . '-' . now(); $paymentRequest->metadata = ['udf1' => "Invoice Ninja"];
$paymentRequest->currency = $this->client->getCurrencyCode();
$request = new PaymentResponseRequest(); $request = new PaymentResponseRequest();
$request->setMethod('POST'); $request->setMethod('POST');
$request->request->add(['payment_hash' => $payment_hash->hash]); $request->request->add(['payment_hash' => $payment_hash->hash]);
try { try {
$response = $this->gateway->payments()->request($payment); // $response = $this->gateway->payments()->request($payment);
$response = $this->gateway->getPaymentsClient()->requestPayment($paymentRequest);
if ($response->status == 'Authorized') { if ($response['status'] == 'Authorized') {
$this->confirmGatewayFee($request); $this->confirmGatewayFee($request);
$data = [ $data = [
'payment_method' => $response->source['id'], 'payment_method' => $response['source']['id'],
'payment_type' => PaymentType::parseCardType(strtolower($response->source['scheme'])), 'payment_type' => PaymentType::parseCardType(strtolower($response['source']['scheme'])),
'amount' => $amount, 'amount' => $amount,
'transaction_reference' => $response->id, 'transaction_reference' => $response['id'],
]; ];
$payment = $this->createPayment($data, Payment::STATUS_COMPLETED); $payment = $this->createPayment($data, Payment::STATUS_COMPLETED);
@ -280,10 +369,10 @@ class CheckoutComPaymentDriver extends BaseDriver
return $payment; return $payment;
} }
if ($response->status == 'Declined') { if ($response['status'] == 'Declined') {
$this->unWindGatewayFees($payment_hash); $this->unWindGatewayFees($payment_hash);
$this->sendFailureMail($response->status . " " . $response->response_summary); $this->sendFailureMail($response['status'] . " " . $response['response_summary']);
$message = [ $message = [
'server_response' => $response, 'server_response' => $response,
@ -300,11 +389,9 @@ class CheckoutComPaymentDriver extends BaseDriver
return false; return false;
} }
} catch (Exception | CheckoutHttpException $e) { } catch (Exception | CheckoutApiException $e) {
$this->unWindGatewayFees($payment_hash); $this->unWindGatewayFees($payment_hash);
$message = $e instanceof CheckoutHttpException $message = $e->getMessage();
? $e->getBody()
: $e->getMessage();
$data = [ $data = [
'status' => '', 'status' => '',
@ -334,20 +421,21 @@ class CheckoutComPaymentDriver extends BaseDriver
public function process3dsConfirmation(Checkout3dsRequest $request) public function process3dsConfirmation(Checkout3dsRequest $request)
{ {
$this->init(); $this->init();
$this->setPaymentHash($request->getPaymentHash()); $this->setPaymentHash($request->getPaymentHash());
try { try {
$payment = $this->gateway->payments()->details( $payment = $this->gateway->getPaymentsClient()->getPaymentDetails(
$request->query('cko-session-id') $request->query('cko-session-id')
); );
if ($payment->approved) { if ($payment['approved']) {
return $this->processSuccessfulPayment($payment); return $this->processSuccessfulPayment($payment);
} else { } else {
return $this->processUnsuccessfulPayment($payment); return $this->processUnsuccessfulPayment($payment);
} }
} catch (CheckoutHttpException | Exception $e) { } catch (CheckoutApiException | Exception $e) {
return $this->processInternallyFailedPayment($this, $e); return $this->processInternallyFailedPayment($this, $e);
} }
} }

View File

@ -235,7 +235,7 @@ class GoCardlessPaymentDriver extends BaseDriver
nlog("GoCardless Event"); nlog("GoCardless Event");
nlog($request->all()); nlog($request->all());
if(!is_array($request->events) || !is_object($request->events)){ if(!$request->has("events")){
nlog("No GoCardless events to process in response?"); nlog("No GoCardless events to process in response?");
return response()->json([], 200); return response()->json([], 200);
@ -245,18 +245,19 @@ class GoCardlessPaymentDriver extends BaseDriver
sleep(1); sleep(1);
foreach ($request->events as $event) { foreach ($request->events as $event) {
if ($event['action'] === 'confirmed' || $event['action'] === 'paid_out' || $event['action'] === 'paid') { if ($event['action'] === 'confirmed' || $event['action'] === 'paid_out') {
nlog("Searching for transaction reference"); nlog("Searching for transaction reference");
$payment = Payment::query() $payment = Payment::query()
->where('transaction_reference', $event['links']['payment']) ->where('transaction_reference', $event['links']['payment'])
// ->where('company_id', $request->getCompany()->id) ->where('company_id', $request->getCompany()->id)
->first(); ->first();
if ($payment) { if ($payment) {
$payment->status_id = Payment::STATUS_COMPLETED; $payment->status_id = Payment::STATUS_COMPLETED;
$payment->save(); $payment->save();
nlog("GoCardless completed");
} }
else else
nlog("I was unable to find the payment for this reference"); nlog("I was unable to find the payment for this reference");
@ -268,12 +269,13 @@ class GoCardlessPaymentDriver extends BaseDriver
$payment = Payment::query() $payment = Payment::query()
->where('transaction_reference', $event['links']['payment']) ->where('transaction_reference', $event['links']['payment'])
// ->where('company_id', $request->getCompany()->id) ->where('company_id', $request->getCompany()->id)
->first(); ->first();
if ($payment) { if ($payment) {
$payment->status_id = Payment::STATUS_FAILED; $payment->status_id = Payment::STATUS_FAILED;
$payment->save(); $payment->save();
nlog("GoCardless completed");
} }
} }
} }

View File

@ -60,11 +60,12 @@ use App\Events\Payment\PaymentWasRefunded;
use App\Events\Payment\PaymentWasRestored; use App\Events\Payment\PaymentWasRestored;
use App\Events\Payment\PaymentWasUpdated; use App\Events\Payment\PaymentWasUpdated;
use App\Events\Payment\PaymentWasVoided; use App\Events\Payment\PaymentWasVoided;
use App\Events\PurchaseOrder\PurchaseOrderWasMarkedSent; use App\Events\PurchaseOrder\PurchaseOrderWasAccepted;
use App\Events\PurchaseOrder\PurchaseOrderWasArchived; use App\Events\PurchaseOrder\PurchaseOrderWasArchived;
use App\Events\PurchaseOrder\PurchaseOrderWasCreated; use App\Events\PurchaseOrder\PurchaseOrderWasCreated;
use App\Events\PurchaseOrder\PurchaseOrderWasDeleted; use App\Events\PurchaseOrder\PurchaseOrderWasDeleted;
use App\Events\PurchaseOrder\PurchaseOrderWasEmailed; use App\Events\PurchaseOrder\PurchaseOrderWasEmailed;
use App\Events\PurchaseOrder\PurchaseOrderWasMarkedSent;
use App\Events\PurchaseOrder\PurchaseOrderWasRestored; use App\Events\PurchaseOrder\PurchaseOrderWasRestored;
use App\Events\PurchaseOrder\PurchaseOrderWasUpdated; use App\Events\PurchaseOrder\PurchaseOrderWasUpdated;
use App\Events\PurchaseOrder\PurchaseOrderWasViewed; use App\Events\PurchaseOrder\PurchaseOrderWasViewed;
@ -179,6 +180,8 @@ use App\Listeners\Payment\PaymentEmailedActivity;
use App\Listeners\Payment\PaymentNotification; use App\Listeners\Payment\PaymentNotification;
use App\Listeners\Payment\PaymentRestoredActivity; use App\Listeners\Payment\PaymentRestoredActivity;
use App\Listeners\PurchaseOrder\CreatePurchaseOrderActivity; use App\Listeners\PurchaseOrder\CreatePurchaseOrderActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderAcceptedActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderAcceptedNotification;
use App\Listeners\PurchaseOrder\PurchaseOrderArchivedActivity; use App\Listeners\PurchaseOrder\PurchaseOrderArchivedActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderDeletedActivity; use App\Listeners\PurchaseOrder\PurchaseOrderDeletedActivity;
use App\Listeners\PurchaseOrder\PurchaseOrderEmailActivity; use App\Listeners\PurchaseOrder\PurchaseOrderEmailActivity;
@ -264,9 +267,9 @@ class EventServiceProvider extends ServiceProvider
* @var array * @var array
*/ */
protected $listen = [ protected $listen = [
AccountCreated::class =>[ AccountCreated::class => [
], ],
MessageSending::class =>[ MessageSending::class => [
], ],
MessageSent::class => [ MessageSent::class => [
MailSentListener::class, MailSentListener::class,
@ -312,35 +315,35 @@ class EventServiceProvider extends ServiceProvider
PaymentWasVoided::class => [ PaymentWasVoided::class => [
PaymentVoidedActivity::class, PaymentVoidedActivity::class,
], ],
PaymentWasRestored::class =>[ PaymentWasRestored::class => [
PaymentRestoredActivity::class, PaymentRestoredActivity::class,
], ],
// Clients // Clients
ClientWasCreated::class =>[ ClientWasCreated::class => [
CreatedClientActivity::class, CreatedClientActivity::class,
], ],
ClientWasArchived::class =>[ ClientWasArchived::class => [
ArchivedClientActivity::class, ArchivedClientActivity::class,
], ],
ClientWasUpdated::class =>[ ClientWasUpdated::class => [
ClientUpdatedActivity::class, ClientUpdatedActivity::class,
], ],
ClientWasDeleted::class =>[ ClientWasDeleted::class => [
DeleteClientActivity::class, DeleteClientActivity::class,
], ],
ClientWasRestored::class =>[ ClientWasRestored::class => [
RestoreClientActivity::class, RestoreClientActivity::class,
], ],
// Documents // Documents
DocumentWasCreated::class =>[ DocumentWasCreated::class => [
], ],
DocumentWasArchived::class =>[ DocumentWasArchived::class => [
], ],
DocumentWasUpdated::class =>[ DocumentWasUpdated::class => [
], ],
DocumentWasDeleted::class =>[ DocumentWasDeleted::class => [
], ],
DocumentWasRestored::class =>[ DocumentWasRestored::class => [
], ],
CreditWasCreated::class => [ CreditWasCreated::class => [
CreatedCreditActivity::class, CreatedCreditActivity::class,
@ -404,11 +407,11 @@ class EventServiceProvider extends ServiceProvider
InvoiceWasCreated::class => [ InvoiceWasCreated::class => [
CreateInvoiceActivity::class, CreateInvoiceActivity::class,
InvoiceCreatedNotification::class, InvoiceCreatedNotification::class,
// CreateInvoicePdf::class, // CreateInvoicePdf::class,
], ],
InvoiceWasPaid::class => [ InvoiceWasPaid::class => [
InvoicePaidActivity::class, InvoicePaidActivity::class,
CreateInvoicePdf::class, CreateInvoicePdf::class,
], ],
InvoiceWasViewed::class => [ InvoiceWasViewed::class => [
InvoiceViewedActivity::class, InvoiceViewedActivity::class,
@ -471,6 +474,10 @@ class EventServiceProvider extends ServiceProvider
PurchaseOrderWasViewed::class => [ PurchaseOrderWasViewed::class => [
PurchaseOrderViewedActivity::class, PurchaseOrderViewedActivity::class,
], ],
PurchaseOrderWasAccepted::class => [
PurchaseOrderAcceptedActivity::class,
PurchaseOrderAcceptedNotification::class
],
CompanyDocumentsDeleted::class => [ CompanyDocumentsDeleted::class => [
DeleteCompanyDocuments::class, DeleteCompanyDocuments::class,
], ],
@ -593,7 +600,12 @@ class EventServiceProvider extends ServiceProvider
], ],
VendorWasUpdated::class => [ VendorWasUpdated::class => [
VendorUpdatedActivity::class, VendorUpdatedActivity::class,
] ],
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
// ... Manager won't register drivers that are not added to this listener.
\SocialiteProviders\Apple\AppleExtendSocialite::class . '@handle',
\SocialiteProviders\Microsoft\MicrosoftExtendSocialite::class . '@handle',
],
]; ];

View File

@ -50,6 +50,8 @@ class RouteServiceProvider extends ServiceProvider
$this->mapContactApiRoutes(); $this->mapContactApiRoutes();
$this->mapVendorsApiRoutes();
$this->mapClientApiRoutes(); $this->mapClientApiRoutes();
$this->mapShopApiRoutes(); $this->mapShopApiRoutes();
@ -121,4 +123,12 @@ class RouteServiceProvider extends ServiceProvider
->namespace($this->namespace) ->namespace($this->namespace)
->group(base_path('routes/shop.php')); ->group(base_path('routes/shop.php'));
} }
protected function mapVendorsApiRoutes()
{
Route::prefix('')
->middleware('client')
->namespace($this->namespace)
->group(base_path('routes/vendor.php'));
}
} }

View File

@ -49,8 +49,6 @@ class GenerateDeliveryNote
$this->contact = $contact; $this->contact = $contact;
// $this->disk = 'public';
$this->disk = $disk ?? config('filesystems.default'); $this->disk = $disk ?? config('filesystems.default');
} }

View File

@ -341,7 +341,7 @@ class InvoiceService
if(Storage::disk(config('filesystems.default'))->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf')) if(Storage::disk(config('filesystems.default'))->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf'))
Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf'); Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf');
if(Ninja::isHosted() && Storage::disk(config('filesystems.default'))->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf')) { if(Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf')) {
Storage::disk('public')->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf'); Storage::disk('public')->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf');
} }

View File

@ -56,6 +56,10 @@ class Design extends BaseDesign
/** @var Payment[] */ /** @var Payment[] */
public $payments; public $payments;
public $settings_object;
public $company;
/** @var array */ /** @var array */
public $aging = []; public $aging = [];
@ -80,6 +84,7 @@ class Design extends BaseDesign
Str::endsWith('.html', $design) ? $this->design = $design : $this->design = "{$design}.html"; Str::endsWith('.html', $design) ? $this->design = $design : $this->design = "{$design}.html";
$this->options = $options; $this->options = $options;
} }
public function html(): ?string public function html(): ?string
@ -574,19 +579,19 @@ class Design extends BaseDesign
foreach ($this->context['pdf_variables']["{$type}_columns"] as $column) { foreach ($this->context['pdf_variables']["{$type}_columns"] as $column) {
if (array_key_exists($column, $aliases)) { if (array_key_exists($column, $aliases)) {
$elements[] = ['element' => 'th', 'content' => $aliases[$column] . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($aliases[$column], 1) . '-th', 'hidden' => $this->client->getSetting('hide_empty_columns_on_pdf')]]; $elements[] = ['element' => 'th', 'content' => $aliases[$column] . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($aliases[$column], 1) . '-th', 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
} elseif ($column == '$product.discount' && !$this->client->company->enable_product_discount) { } elseif ($column == '$product.discount' && !$this->company->enable_product_discount) {
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'style' => 'display: none;']]; $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'style' => 'display: none;']];
} elseif ($column == '$product.quantity' && !$this->client->company->enable_product_quantity) { } elseif ($column == '$product.quantity' && !$this->company->enable_product_quantity) {
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'style' => 'display: none;']]; $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'style' => 'display: none;']];
} elseif ($column == '$product.tax_rate1') { } elseif ($column == '$product.tax_rate1') {
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax1-th", 'hidden' => $this->client->getSetting('hide_empty_columns_on_pdf')]]; $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax1-th", 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
} elseif ($column == '$product.tax_rate2') { } elseif ($column == '$product.tax_rate2') {
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax2-th", 'hidden' => $this->client->getSetting('hide_empty_columns_on_pdf')]]; $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax2-th", 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
} elseif ($column == '$product.tax_rate3') { } elseif ($column == '$product.tax_rate3') {
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax3-th", 'hidden' => $this->client->getSetting('hide_empty_columns_on_pdf')]]; $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-product.tax3-th", 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
} else { } else {
$elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'hidden' => $this->client->getSetting('hide_empty_columns_on_pdf')]]; $elements[] = ['element' => 'th', 'content' => $column . '_label', 'properties' => ['data-ref' => "{$type}_table-" . substr($column, 1) . '-th', 'hidden' => $this->settings_object->getSetting('hide_empty_columns_on_pdf')]];
} }
} }
@ -677,9 +682,9 @@ class Design extends BaseDesign
if ($cell == '$task.rate') { if ($cell == '$task.rate') {
$element['elements'][] = ['element' => 'td', 'content' => $row['$task.cost'], 'properties' => ['data-ref' => 'task_table-task.cost-td']]; $element['elements'][] = ['element' => 'td', 'content' => $row['$task.cost'], 'properties' => ['data-ref' => 'task_table-task.cost-td']];
} elseif ($cell == '$product.discount' && !$this->client->company->enable_product_discount) { } elseif ($cell == '$product.discount' && !$this->company->enable_product_discount) {
$element['elements'][] = ['element' => 'td', 'content' => $row['$product.discount'], 'properties' => ['data-ref' => 'product_table-product.discount-td', 'style' => 'display: none;']]; $element['elements'][] = ['element' => 'td', 'content' => $row['$product.discount'], 'properties' => ['data-ref' => 'product_table-product.discount-td', 'style' => 'display: none;']];
} elseif ($cell == '$product.quantity' && !$this->client->company->enable_product_quantity) { } elseif ($cell == '$product.quantity' && !$this->company->enable_product_quantity) {
$element['elements'][] = ['element' => 'td', 'content' => $row['$product.quantity'], 'properties' => ['data-ref' => 'product_table-product.quantity-td', 'style' => 'display: none;']]; $element['elements'][] = ['element' => 'td', 'content' => $row['$product.quantity'], 'properties' => ['data-ref' => 'product_table-product.quantity-td', 'style' => 'display: none;']];
} elseif ($cell == '$task.hours') { } elseif ($cell == '$task.hours') {
$element['elements'][] = ['element' => 'td', 'content' => $row['$task.quantity'], 'properties' => ['data-ref' => 'task_table-task.hours-td']]; $element['elements'][] = ['element' => 'td', 'content' => $row['$task.quantity'], 'properties' => ['data-ref' => 'task_table-task.hours-td']];
@ -799,7 +804,7 @@ class Design extends BaseDesign
} elseif (Str::startsWith($variable, '$custom_surcharge')) { } elseif (Str::startsWith($variable, '$custom_surcharge')) {
$_variable = ltrim($variable, '$'); // $custom_surcharge1 -> custom_surcharge1 $_variable = ltrim($variable, '$'); // $custom_surcharge1 -> custom_surcharge1
$visible = $this->entity->{$_variable} != 0 || $this->entity->{$_variable} != '0'; $visible = (int)$this->entity->{$_variable} != 0 || $this->entity->{$_variable} != '0' || !$this->entity->{$_variable};
$elements[1]['elements'][] = ['element' => 'div', 'elements' => [ $elements[1]['elements'][] = ['element' => 'div', 'elements' => [
['element' => 'span', 'content' => $variable . '_label', 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1) . '-label']], ['element' => 'span', 'content' => $variable . '_label', 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1) . '-label']],
@ -807,7 +812,7 @@ class Design extends BaseDesign
]]; ]];
} elseif (Str::startsWith($variable, '$custom')) { } elseif (Str::startsWith($variable, '$custom')) {
$field = explode('_', $variable); $field = explode('_', $variable);
$visible = is_object($this->client->company->custom_fields) && property_exists($this->client->company->custom_fields, $field[1]) && !empty($this->client->company->custom_fields->{$field[1]}); $visible = is_object($this->company->custom_fields) && property_exists($this->company->custom_fields, $field[1]) && !empty($this->company->custom_fields->{$field[1]});
$elements[1]['elements'][] = ['element' => 'div', 'elements' => [ $elements[1]['elements'][] = ['element' => 'div', 'elements' => [
['element' => 'span', 'content' => $variable . '_label', 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1) . '-label']], ['element' => 'span', 'content' => $variable . '_label', 'properties' => ['hidden' => !$visible, 'data-ref' => 'totals_table-' . substr($variable, 1) . '-label']],

View File

@ -60,6 +60,10 @@ trait DesignHelpers
$this->document(); $this->document();
$this->settings_object = $this->vendor ? $this->vendor->company : $this->client;
$this->company = $this->vendor ? $this->vendor->company : $this->client->company;
return $this; return $this;
} }
@ -180,7 +184,7 @@ trait DesignHelpers
$key = array_search(sprintf('%s%s.tax', '$', $type), $this->context['pdf_variables']["{$type}_columns"], true); $key = array_search(sprintf('%s%s.tax', '$', $type), $this->context['pdf_variables']["{$type}_columns"], true);
if ($key) { if ($key !== false) {
array_splice($this->context['pdf_variables']["{$type}_columns"], $key, 1, $taxes); array_splice($this->context['pdf_variables']["{$type}_columns"], $key, 1, $taxes);
} }
} }
@ -338,7 +342,7 @@ document.addEventListener('DOMContentLoaded', function() {
$key = array_search(sprintf('%s%s.description', '$', $type), $this->context['pdf_variables']["{$type}_columns"], true); $key = array_search(sprintf('%s%s.description', '$', $type), $this->context['pdf_variables']["{$type}_columns"], true);
if ($key) { if ($key !== false) {
array_splice($this->context['pdf_variables']["{$type}_columns"], $key + 1, 0, $custom_columns); array_splice($this->context['pdf_variables']["{$type}_columns"], $key + 1, 0, $custom_columns);
} }
} }

View File

@ -42,7 +42,7 @@ class CreateInvitations extends AbstractService
public function run() public function run()
{ {
$contacts = $this->purchase_order->vendor->contacts()->where('send_email', true)->get(); $contacts = $this->purchase_order->vendor->contacts()->get();
if($contacts->count() == 0){ if($contacts->count() == 0){
$this->createBlankContact(); $this->createBlankContact();

View File

@ -31,7 +31,7 @@ class GetPurchaseOrderPdf extends AbstractService
{ {
if (! $this->contact) { if (! $this->contact) {
$this->contact = $this->purchase_order->vendor->contacts()->where('send_email', true)->first(); $this->contact = $this->purchase_order->vendor->contacts()->orderBy('send_email', 'DESC')->first();
} }
$invitation = $this->purchase_order->invitations()->where('vendor_contact_id', $this->contact->id)->first(); $invitation = $this->purchase_order->invitations()->where('vendor_contact_id', $this->contact->id)->first();

View File

@ -11,11 +11,12 @@
namespace App\Services\PurchaseOrder; namespace App\Services\PurchaseOrder;
use App\Jobs\Vendor\CreatePurchaseOrderPdf;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Services\PurchaseOrder\ApplyNumber; use App\Services\PurchaseOrder\ApplyNumber;
use App\Services\PurchaseOrder\CreateInvitations; use App\Services\PurchaseOrder\CreateInvitations;
use App\Services\PurchaseOrder\GetPurchaseOrderPdf; use App\Services\PurchaseOrder\GetPurchaseOrderPdf;
use App\Services\PurchaseOrder\TriggeredActions;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
class PurchaseOrderService class PurchaseOrderService
@ -62,6 +63,13 @@ class PurchaseOrderService
return $this; return $this;
} }
public function triggeredActions($request)
{
$this->purchase_order = (new TriggeredActions($this->purchase_order->load('invitations'), $request))->run();
return $this;
}
public function getPurchaseOrderPdf($contact = null) public function getPurchaseOrderPdf($contact = null)
{ {
return (new GetPurchaseOrderPdf($this->purchase_order, $contact))->run(); return (new GetPurchaseOrderPdf($this->purchase_order, $contact))->run();
@ -81,6 +89,34 @@ class PurchaseOrderService
return $this; return $this;
} }
public function touchPdf($force = false)
{
try {
if($force){
$this->purchase_order->invitations->each(function ($invitation) {
CreatePurchaseOrderPdf::dispatchNow($invitation);
});
return $this;
}
$this->purchase_order->invitations->each(function ($invitation) {
CreatePurchaseOrderPdf::dispatch($invitation);
});
}
catch(\Exception $e){
nlog("failed creating purchase orders in Touch PDF");
}
return $this;
}
/** /**
* Saves the purchase order. * Saves the purchase order.
* @return \App\Models\PurchaseOrder object * @return \App\Models\PurchaseOrder object

View File

@ -0,0 +1,65 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\PurchaseOrder;
use App\Events\Invoice\InvoiceWasEmailed;
use App\Events\PurchaseOrder\PurchaseOrderWasEmailed;
use App\Jobs\Entity\EmailEntity;
use App\Jobs\PurchaseOrder\PurchaseOrderEmail;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Services\AbstractService;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use Illuminate\Http\Request;
class TriggeredActions extends AbstractService
{
use GeneratesCounter;
private $request;
private $purchase_order;
public function __construct(PurchaseOrder $purchase_order, Request $request)
{
$this->request = $request;
$this->purchase_order = $purchase_order;
}
public function run()
{
if ($this->request->has('send_email') && $this->request->input('send_email') == 'true') {
$this->purchase_order->service()->markSent()->touchPdf()->save();
$this->sendEmail();
}
if ($this->request->has('mark_sent') && $this->request->input('mark_sent') == 'true') {
$this->purchase_order = $this->purchase_order->service()->markSent()->touchPdf()->save();
}
// if ($this->request->has('cancel') && $this->request->input('cancel') == 'true') {
// $this->purchase_order = $this->purchase_order->service()->handleCancellation()->save();
// }
return $this->purchase_order;
}
private function sendEmail()
{
PurchaseOrderEmail::dispatch($this->purchase_order, $this->purchase_order->company);
}
}

View File

@ -164,6 +164,7 @@ class SubscriptionService
$recurring_invoice = $this->convertInvoiceToRecurring($client_contact->client_id); $recurring_invoice = $this->convertInvoiceToRecurring($client_contact->client_id);
$recurring_invoice->next_send_date = now()->addSeconds($this->subscription->trial_duration); $recurring_invoice->next_send_date = now()->addSeconds($this->subscription->trial_duration);
$recurring_invoice->next_send_date_client = now()->addSeconds($this->subscription->trial_duration);
$recurring_invoice->backup = 'is_trial'; $recurring_invoice->backup = 'is_trial';
if(array_key_exists('coupon', $data) && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) if(array_key_exists('coupon', $data) && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0)
@ -620,7 +621,9 @@ class SubscriptionService
$recurring_invoice = $this->convertInvoiceToRecurring($old_recurring_invoice->client_id); $recurring_invoice = $this->convertInvoiceToRecurring($old_recurring_invoice->client_id);
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice); $recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
$recurring_invoice->next_send_date = now()->format('Y-m-d'); $recurring_invoice->next_send_date = now()->format('Y-m-d');
$recurring_invoice->next_send_date_client = now()->format('Y-m-d');
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
/* Start the recurring service */ /* Start the recurring service */
$recurring_invoice->service() $recurring_invoice->service()
@ -754,8 +757,9 @@ class SubscriptionService
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill); $recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
$recurring_invoice->due_date_days = 'terms'; $recurring_invoice->due_date_days = 'terms';
$recurring_invoice->next_send_date = now()->format('Y-m-d'); $recurring_invoice->next_send_date = now()->format('Y-m-d');
$recurring_invoice->next_send_date_client = now()->format('Y-m-d');
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
return $recurring_invoice; return $recurring_invoice;
} }

View File

@ -58,7 +58,10 @@ class ZeroCostProduct extends AbstractService
$recurring_invoice->next_send_date = now(); $recurring_invoice->next_send_date = now();
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice); $recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
$recurring_invoice->next_send_date = now();
$recurring_invoice->next_send_date_client = now();
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate(); $recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
$recurring_invoice->next_send_date_client = $recurring_invoice->nextSendDateClient();
/* Start the recurring service */ /* Start the recurring service */
$recurring_invoice->service() $recurring_invoice->service()

View File

@ -86,6 +86,7 @@ class AccountTransformer extends EntityTransformer
'hosted_client_count' => (int) $account->hosted_client_count, 'hosted_client_count' => (int) $account->hosted_client_count,
'hosted_company_count' => (int) $account->hosted_company_count, 'hosted_company_count' => (int) $account->hosted_company_count,
'is_hosted' => (bool) Ninja::isHosted(), 'is_hosted' => (bool) Ninja::isHosted(),
'set_react_as_default_ap' => (bool) $account->set_react_as_default_ap
]; ];
} }

View File

@ -65,7 +65,7 @@ class ActivityTransformer extends EntityTransformer
'created_at' => (int) $activity->created_at, 'created_at' => (int) $activity->created_at,
'expense_id' => $activity->expense_id ? (string) $this->encodePrimaryKey($activity->expense_id) : '', 'expense_id' => $activity->expense_id ? (string) $this->encodePrimaryKey($activity->expense_id) : '',
'is_system' => (bool) $activity->is_system, 'is_system' => (bool) $activity->is_system,
'contact_id' => $activity->contact_id ? (string) $this->encodePrimaryKey($activity->contact_id) : '', 'contact_id' => $activity->client_contact_id ? (string) $this->encodePrimaryKey($activity->client_contact_id) : '',
'task_id' => $activity->task_id ? (string) $this->encodePrimaryKey($activity->task_id) : '', 'task_id' => $activity->task_id ? (string) $this->encodePrimaryKey($activity->task_id) : '',
'token_id' => $activity->token_id ? (string) $this->encodePrimaryKey($activity->token_id) : '', 'token_id' => $activity->token_id ? (string) $this->encodePrimaryKey($activity->token_id) : '',
'notes' => $activity->notes ? (string) $activity->notes : '', 'notes' => $activity->notes ? (string) $activity->notes : '',

View File

@ -29,6 +29,7 @@ use App\Models\Payment;
use App\Models\PaymentTerm; use App\Models\PaymentTerm;
use App\Models\Product; use App\Models\Product;
use App\Models\Project; use App\Models\Project;
use App\Models\PurchaseOrder;
use App\Models\Quote; use App\Models\Quote;
use App\Models\RecurringExpense; use App\Models\RecurringExpense;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
@ -39,6 +40,7 @@ use App\Models\TaskStatus;
use App\Models\TaxRate; use App\Models\TaxRate;
use App\Models\User; use App\Models\User;
use App\Models\Webhook; use App\Models\Webhook;
use App\Transformers\PurchaseOrderTransformer;
use App\Transformers\RecurringExpenseTransformer; use App\Transformers\RecurringExpenseTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use stdClass; use stdClass;
@ -95,6 +97,7 @@ class CompanyTransformer extends EntityTransformer
'task_statuses', 'task_statuses',
'subscriptions', 'subscriptions',
'recurring_expenses', 'recurring_expenses',
'purchase_orders',
]; ];
/** /**
@ -391,4 +394,11 @@ class CompanyTransformer extends EntityTransformer
return $this->includeCollection($company->subscriptions, $transformer, Subscription::class); return $this->includeCollection($company->subscriptions, $transformer, Subscription::class);
} }
public function includePurchaseOrders(Company $company)
{
$transformer = new PurchaseOrderTransformer($this->serializer);
return $this->includeCollection($company->purchase_orders, $transformer, PurchaseOrder::class);
}
} }

View File

@ -265,6 +265,8 @@ trait MakesInvoiceValues
*/ */
public function transformLineItems($items, $table_type = '$product') :array public function transformLineItems($items, $table_type = '$product') :array
{ {
$entity = $this->client ? $this->client : $this->company;
$data = []; $data = [];
if (! is_array($items)) { if (! is_array($items)) {
@ -294,23 +296,23 @@ trait MakesInvoiceValues
$data[$key][$table_type.'.item'] = is_null(optional($item)->item) ? $item->product_key : $item->item; $data[$key][$table_type.'.item'] = is_null(optional($item)->item) ? $item->product_key : $item->item;
$data[$key][$table_type.'.service'] = is_null(optional($item)->service) ? $item->product_key : $item->service; $data[$key][$table_type.'.service'] = is_null(optional($item)->service) ? $item->product_key : $item->service;
$data[$key][$table_type.'.notes'] = Helpers::processReservedKeywords($item->notes, $this->client); $data[$key][$table_type.'.notes'] = Helpers::processReservedKeywords($item->notes, $entity);
$data[$key][$table_type.'.description'] = Helpers::processReservedKeywords($item->notes, $this->client); $data[$key][$table_type.'.description'] = Helpers::processReservedKeywords($item->notes, $entity);
$data[$key][$table_type . ".{$_table_type}1"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}1", $item->custom_value1, $this->client); $data[$key][$table_type . ".{$_table_type}1"] = $helpers->formatCustomFieldValue($this->company->custom_fields, "{$_table_type}1", $item->custom_value1, $entity);
$data[$key][$table_type . ".{$_table_type}2"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}2", $item->custom_value2, $this->client); $data[$key][$table_type . ".{$_table_type}2"] = $helpers->formatCustomFieldValue($this->company->custom_fields, "{$_table_type}2", $item->custom_value2, $entity);
$data[$key][$table_type . ".{$_table_type}3"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}3", $item->custom_value3, $this->client); $data[$key][$table_type . ".{$_table_type}3"] = $helpers->formatCustomFieldValue($this->company->custom_fields, "{$_table_type}3", $item->custom_value3, $entity);
$data[$key][$table_type . ".{$_table_type}4"] = $helpers->formatCustomFieldValue($this->client->company->custom_fields, "{$_table_type}4", $item->custom_value4, $this->client); $data[$key][$table_type . ".{$_table_type}4"] = $helpers->formatCustomFieldValue($this->company->custom_fields, "{$_table_type}4", $item->custom_value4, $entity);
if($item->quantity > 0 || $item->cost > 0){ if($item->quantity > 0 || $item->cost > 0){
$data[$key][$table_type.'.quantity'] = Number::formatValueNoTrailingZeroes($item->quantity, $this->client->currency()); $data[$key][$table_type.'.quantity'] = Number::formatValueNoTrailingZeroes($item->quantity, $entity->currency());
$data[$key][$table_type.'.unit_cost'] = Number::formatMoneyNoRounding($item->cost, $this->client); $data[$key][$table_type.'.unit_cost'] = Number::formatMoneyNoRounding($item->cost, $entity);
$data[$key][$table_type.'.cost'] = Number::formatMoney($item->cost, $this->client); $data[$key][$table_type.'.cost'] = Number::formatMoney($item->cost, $entity);
$data[$key][$table_type.'.line_total'] = Number::formatMoney($item->line_total, $this->client); $data[$key][$table_type.'.line_total'] = Number::formatMoney($item->line_total, $entity);
} }
else { else {
@ -326,13 +328,13 @@ trait MakesInvoiceValues
} }
if(property_exists($item, 'gross_line_total')) if(property_exists($item, 'gross_line_total'))
$data[$key][$table_type.'.gross_line_total'] = ($item->gross_line_total == 0) ? '' :Number::formatMoney($item->gross_line_total, $this->client); $data[$key][$table_type.'.gross_line_total'] = ($item->gross_line_total == 0) ? '' :Number::formatMoney($item->gross_line_total, $entity);
else else
$data[$key][$table_type.'.gross_line_total'] = ''; $data[$key][$table_type.'.gross_line_total'] = '';
if (isset($item->discount) && $item->discount > 0) { if (isset($item->discount) && $item->discount > 0) {
if ($item->is_amount_discount) { if ($item->is_amount_discount) {
$data[$key][$table_type.'.discount'] = Number::formatMoney($item->discount, $this->client); $data[$key][$table_type.'.discount'] = Number::formatMoney($item->discount, $entity);
} else { } else {
$data[$key][$table_type.'.discount'] = floatval($item->discount).'%'; $data[$key][$table_type.'.discount'] = floatval($item->discount).'%';
} }
@ -376,13 +378,14 @@ trait MakesInvoiceValues
private function makeLineTaxes() :string private function makeLineTaxes() :string
{ {
$tax_map = $this->calc()->getTaxMap(); $tax_map = $this->calc()->getTaxMap();
$entity = $this->client ? $this->client : $this->company;
$data = ''; $data = '';
foreach ($tax_map as $tax) { foreach ($tax_map as $tax) {
$data .= '<tr class="line_taxes">'; $data .= '<tr class="line_taxes">';
$data .= '<td>'.$tax['name'].'</td>'; $data .= '<td>'.$tax['name'].'</td>';
$data .= '<td>'.Number::formatMoney($tax['total'], $this->client).'</td></tr>'; $data .= '<td>'.Number::formatMoney($tax['total'], $entity).'</td></tr>';
} }
return $data; return $data;
@ -395,6 +398,7 @@ trait MakesInvoiceValues
private function makeTotalTaxes() :string private function makeTotalTaxes() :string
{ {
$data = ''; $data = '';
$entity = $this->client ? $this->client : $this->company;
if (! $this->calc()->getTotalTaxMap()) { if (! $this->calc()->getTotalTaxMap()) {
return $data; return $data;
@ -403,7 +407,7 @@ trait MakesInvoiceValues
foreach ($this->calc()->getTotalTaxMap() as $tax) { foreach ($this->calc()->getTotalTaxMap() as $tax) {
$data .= '<tr class="total_taxes">'; $data .= '<tr class="total_taxes">';
$data .= '<td>'.$tax['name'].'</td>'; $data .= '<td>'.$tax['name'].'</td>';
$data .= '<td>'.Number::formatMoney($tax['total'], $this->client).'</td></tr>'; $data .= '<td>'.Number::formatMoney($tax['total'], $entity).'</td></tr>';
} }
return $data; return $data;
@ -427,13 +431,14 @@ trait MakesInvoiceValues
private function totalTaxValues() :string private function totalTaxValues() :string
{ {
$data = ''; $data = '';
$entity = $this->client ? $this->client : $this->company;
if (! $this->calc()->getTotalTaxMap()) { if (! $this->calc()->getTotalTaxMap()) {
return $data; return $data;
} }
foreach ($this->calc()->getTotalTaxMap() as $tax) { foreach ($this->calc()->getTotalTaxMap() as $tax) {
$data .= '<span>'.Number::formatMoney($tax['total'], $this->client).'</span>'; $data .= '<span>'.Number::formatMoney($tax['total'], $entity).'</span>';
} }
return $data; return $data;
@ -455,11 +460,12 @@ trait MakesInvoiceValues
private function lineTaxValues() :string private function lineTaxValues() :string
{ {
$tax_map = $this->calc()->getTaxMap(); $tax_map = $this->calc()->getTaxMap();
$entity = $this->client ? $this->client : $this->company;
$data = ''; $data = '';
foreach ($tax_map as $tax) { foreach ($tax_map as $tax) {
$data .= '<span>'.Number::formatMoney($tax['total'], $this->client).'</span>'; $data .= '<span>'.Number::formatMoney($tax['total'], $entity).'</span>';
} }
return $data; return $data;
@ -481,7 +487,8 @@ trait MakesInvoiceValues
*/ */
public function generateCustomCSS() :string public function generateCustomCSS() :string
{ {
$settings = $this->client->getMergedSettings();
$settings = $this->client ? $this->client->getMergedSettings() : $this->company->settings;
$header_and_footer = ' $header_and_footer = '
.header, .header-space { .header, .header-space {

View File

@ -15,6 +15,7 @@ use App\Models\Client;
use App\Models\Credit; use App\Models\Credit;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PurchaseOrder;
use App\Models\Quote; use App\Models\Quote;
/** /**
@ -99,7 +100,10 @@ trait UserNotifies
break; break;
case ($entity instanceof Credit): case ($entity instanceof Credit):
return array_merge($required_permissions, ["all_notifications","all_user_notifications","credit_created_user","credit_sent_user","credit_viewed_user"]); return array_merge($required_permissions, ["all_notifications","all_user_notifications","credit_created_user","credit_sent_user","credit_viewed_user"]);
break; break;
case ($entity instanceof PurchaseOrder):
return array_merge($required_permissions, ["all_notifications","all_user_notifications","purchase_order_created_user","purchase_order_sent_user","purchase_order_viewed_user"]);
break;
default: default:
return []; return [];
break; break;
@ -122,7 +126,10 @@ trait UserNotifies
break; break;
case ($entity instanceof Credit): case ($entity instanceof Credit):
return array_diff($required_permissions, ["all_user_notifications","credit_created_user","credit_sent_user","credit_viewed_user"]); return array_diff($required_permissions, ["all_user_notifications","credit_created_user","credit_sent_user","credit_viewed_user"]);
break; break;
case ($entity instanceof PurchaseOrder):
return array_diff($required_permissions, ["all_user_notifications","purchase_order_created_user","purchase_order_sent_user","purchase_order_viewed_user"]);
break;
default: default:
// code... // code...
break; break;

View File

@ -134,20 +134,15 @@ class VendorHtmlEngine
$data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->company->date_format(), $this->company->locale()), 'label' => ctrans('texts.date')]; $data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->company->date_format(), $this->company->locale()), 'label' => ctrans('texts.date')];
$data['$payment_button'] = ['value' => '<a class="button" href="'.$this->invitation->getPaymentLink().'">'.ctrans('texts.pay_now').'</a>', 'label' => ctrans('texts.pay_now')];
$data['$payment_link'] = ['value' => $this->invitation->getPaymentLink(), 'label' => ctrans('texts.pay_now')];
$data['$entity'] = ['value' => '', 'label' => ctrans('texts.purchase_order')]; $data['$entity'] = ['value' => '', 'label' => ctrans('texts.purchase_order')];
$data['$number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.purchase_order_number')]; $data['$number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.purchase_order_number')];
$data['$number_short'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.purchase_order_number_short')]; $data['$number_short'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.purchase_order_number_short')];
$data['$entity.terms'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->terms), $this->company) ?: '', 'label' => ctrans('texts.invoice_terms')]; $data['$entity.terms'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->terms), $this->company) ?: '', 'label' => ctrans('texts.invoice_terms')];
$data['$terms'] = &$data['$entity.terms']; $data['$terms'] = &$data['$entity.terms'];
$data['$view_link'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_invoice').'</a>', 'label' => ctrans('texts.view_invoice')]; $data['$view_link'] = ['value' => '<a class="button" href="'.$this->invitation->getLink().'">'.ctrans('texts.view_purchase_order').'</a>', 'label' => ctrans('texts.view_purchase_order')];
$data['$viewLink'] = &$data['$view_link']; $data['$viewLink'] = &$data['$view_link'];
$data['$viewButton'] = &$data['$view_link']; $data['$viewButton'] = &$data['$view_link'];
$data['$view_button'] = &$data['$view_link']; $data['$view_button'] = &$data['$view_link'];
$data['$paymentButton'] = &$data['$payment_button'];
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')]; $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()) ?: '&nbsp;', 'label' => ctrans('texts.date')]; $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->company->date_format(), $this->company->locale()) ?: '&nbsp;', 'label' => ctrans('texts.date')];
@ -390,11 +385,6 @@ class VendorHtmlEngine
$data['$autoBill'] = ['value' => ctrans('texts.auto_bill_notification_placeholder'), 'label' => '']; $data['$autoBill'] = ['value' => ctrans('texts.auto_bill_notification_placeholder'), 'label' => ''];
$data['$auto_bill'] = &$data['$autoBill']; $data['$auto_bill'] = &$data['$autoBill'];
/*Payment Aliases*/
$data['$paymentLink'] = &$data['$payment_link'];
$data['$payment_url'] = &$data['$payment_link'];
$data['$portalButton'] = &$data['$paymentLink'];
$data['$dir'] = ['value' => optional($this->company->language())->locale === 'ar' ? 'rtl' : 'ltr', 'label' => '']; $data['$dir'] = ['value' => optional($this->company->language())->locale === 'ar' ? 'rtl' : 'ltr', 'label' => ''];
$data['$dir_text_align'] = ['value' => optional($this->company->language())->locale === 'ar' ? 'right' : 'left', 'label' => '']; $data['$dir_text_align'] = ['value' => optional($this->company->language())->locale === 'ar' ? 'right' : 'left', 'label' => ''];

View File

@ -11,7 +11,12 @@
"Credit card billing", "Credit card billing",
"projects", "projects",
"tasks", "tasks",
"freelancer" "freelancer",
"quotes",
"purchase orders",
"stripe billing",
"invoices",
"subscriptions"
], ],
"license": "Elastic License", "license": "Elastic License",
"authors": [ "authors": [
@ -37,7 +42,7 @@
"bacon/bacon-qr-code": "^2.0", "bacon/bacon-qr-code": "^2.0",
"beganovich/snappdf": "^1.7", "beganovich/snappdf": "^1.7",
"braintree/braintree_php": "^6.0", "braintree/braintree_php": "^6.0",
"checkout/checkout-sdk-php": "^1.0", "checkout/checkout-sdk-php": "^2.5",
"cleverit/ubl_invoice": "^1.3", "cleverit/ubl_invoice": "^1.3",
"coconutcraig/laravel-postmark": "^2.10", "coconutcraig/laravel-postmark": "^2.10",
"doctrine/dbal": "^3.0", "doctrine/dbal": "^3.0",
@ -77,6 +82,8 @@
"sentry/sentry-laravel": "^2", "sentry/sentry-laravel": "^2",
"setasign/fpdf": "^1.8", "setasign/fpdf": "^1.8",
"setasign/fpdi": "^2.3", "setasign/fpdi": "^2.3",
"socialiteproviders/apple": "^5.2",
"socialiteproviders/microsoft": "^4.1",
"square/square": "13.0.0.20210721", "square/square": "13.0.0.20210721",
"stripe/stripe-php": "^7.50", "stripe/stripe-php": "^7.50",
"symfony/http-client": "^5.2", "symfony/http-client": "^5.2",

1012
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -56,6 +56,10 @@ return [
'driver' => 'session', 'driver' => 'session',
'provider' => 'contacts', 'provider' => 'contacts',
], ],
'vendor' => [
'driver' => 'session',
'provider' => 'vendors',
],
], ],
/* /*
@ -85,6 +89,11 @@ return [
'driver' => 'eloquent', 'driver' => 'eloquent',
'model' => App\Models\ClientContact::class, 'model' => App\Models\ClientContact::class,
], ],
'vendors' => [
'driver' => 'eloquent',
'model' => App\Models\VendorContact::class,
],
// 'users' => [ // 'users' => [
// 'driver' => 'database', // 'driver' => 'database',
@ -120,6 +129,11 @@ return [
'table' => 'password_resets', 'table' => 'password_resets',
'expire' => 60, 'expire' => 60,
], ],
'vendors' => [
'provider' => 'vendors',
'table' => 'password_resets',
'expire' => 60,
],
], ],
/* /*

View File

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

View File

@ -80,4 +80,14 @@ return [
'postmark' => [ 'postmark' => [
'token' => env('POSTMARK_SECRET'), 'token' => env('POSTMARK_SECRET'),
], ],
'microsoft' => [
'client_id' => env('MICROSOFT_CLIENT_ID'),
'client_secret' => env('MICROSOFT_CLIENT_SECRET'),
'redirect' => env('MICROSOFT_REDIRECT_URI')
],
'apple' => [
'client_id' => env('APPLE_CLIENT_ID'),
'client_secret' => env('APPLE_CLIENT_SECRET'),
'redirect' => env('APPLE_REDIRECT_URI')
],
]; ];

View File

@ -0,0 +1,37 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class SetAccountFlagForReact extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Illuminate\Support\Facades\Artisan::call('ninja:design-update');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddReactSwitchingFlag extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('accounts', function (Blueprint $table) {
$table->boolean('set_react_as_default_ap')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -5,9 +5,9 @@ const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = { const RESOURCES = {
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed", "icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35", "icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"/": "f4932ba24bfa72c97f7578b8951891c2", "/": "2e739a78eec983322924f724ebfa09ba",
"main.dart.js": "68a04477f6ce39dcf894f583120e1c46", "main.dart.js": "fa4a0263712be1ce1df7d59ca0ede10e",
"version.json": "3afb81924daf4f751571755436069115", "version.json": "d72bd323e3b8e22ce5acdc247f4e6f62",
"favicon.png": "dca91c54388f52eded692718d5a98b8b", "favicon.png": "dca91c54388f52eded692718d5a98b8b",
"flutter.js": "0816e65a103ba8ba51b174eeeeb2cb67", "flutter.js": "0816e65a103ba8ba51b174eeeeb2cb67",
"favicon.ico": "51636d3a390451561744c42188ccd628", "favicon.ico": "51636d3a390451561744c42188ccd628",

View File

@ -0,0 +1,2 @@
/*! For license information please see accept.js.LICENSE.txt */
(()=>{function e(e,t){for(var n=0;n<t.length;n++){var a=t[n];a.enumerable=a.enumerable||!1,a.configurable=!0,"value"in a&&(a.writable=!0),Object.defineProperty(e,a.key,a)}}var t=function(){function t(e,n){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,t),this.shouldDisplaySignature=e,this.shouldDisplayTerms=n,this.termsAccepted=!1}var n,a,r;return n=t,(a=[{key:"submitForm",value:function(){document.getElementById("approve-form").submit()}},{key:"displaySignature",value:function(){document.getElementById("displaySignatureModal").removeAttribute("style");var e=new SignaturePad(document.getElementById("signature-pad"),{penColor:"rgb(0, 0, 0)"});this.signaturePad=e}},{key:"displayTerms",value:function(){document.getElementById("displayTermsModal").removeAttribute("style")}},{key:"handle",value:function(){var e=this;document.getElementById("approve-button").addEventListener("click",(function(){e.shouldDisplaySignature&&e.shouldDisplayTerms&&(e.displaySignature(),document.getElementById("signature-next-step").addEventListener("click",(function(){e.displayTerms(),document.getElementById("accept-terms-button").addEventListener("click",(function(){document.querySelector('input[name="signature"').value=e.signaturePad.toDataURL(),e.termsAccepted=!0,e.submitForm()}))}))),e.shouldDisplaySignature&&!e.shouldDisplayTerms&&(e.displaySignature(),document.getElementById("signature-next-step").addEventListener("click",(function(){document.querySelector('input[name="signature"').value=e.signaturePad.toDataURL(),e.submitForm()}))),!e.shouldDisplaySignature&&e.shouldDisplayTerms&&(e.displayTerms(),document.getElementById("accept-terms-button").addEventListener("click",(function(){e.termsAccepted=!0,e.submitForm()}))),e.shouldDisplaySignature||e.shouldDisplayTerms||e.submitForm()}))}}])&&e(n.prototype,a),r&&e(n,r),Object.defineProperty(n,"prototype",{writable:!1}),t}(),n=document.querySelector('meta[name="require-purchase_order-signature"]').content,a=document.querySelector('meta[name="show-purchase_order-terms"]').content;new t(Boolean(+n),Boolean(+a)).handle()})();

View File

@ -0,0 +1,9 @@
/**
* 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
*/

View File

@ -0,0 +1,2 @@
/*! For license information please see action-selectors.js.LICENSE.txt */
(()=>{function e(e,n){var r="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!r){if(Array.isArray(e)||(r=function(e,n){if(!e)return;if("string"==typeof e)return t(e,n);var r=Object.prototype.toString.call(e).slice(8,-1);"Object"===r&&e.constructor&&(r=e.constructor.name);if("Map"===r||"Set"===r)return Array.from(e);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return t(e,n)}(e))||n&&e&&"number"==typeof e.length){r&&(e=r);var o=0,c=function(){};return{s:c,n:function(){return o>=e.length?{done:!0}:{done:!1,value:e[o++]}},e:function(e){throw e},f:c}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,a=!0,l=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return a=e.done,e},e:function(e){l=!0,i=e},f:function(){try{a||null==r.return||r.return()}finally{if(l)throw i}}}}function t(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}(new(function(){function t(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,t),this.parentElement=document.querySelector(".form-check-parent"),this.parentForm=document.getElementById("bulkActions")}var r,o,c;return r=t,o=[{key:"watchCheckboxes",value:function(e){var t=this;document.querySelectorAll(".child-hidden-input").forEach((function(e){return e.remove()})),document.querySelectorAll(".form-check-child").forEach((function(n){e.checked?(n.checked=e.checked,t.processChildItem(n,document.getElementById("bulkActions"))):(n.checked=!1,document.querySelectorAll(".child-hidden-input").forEach((function(e){return e.remove()})))}))}},{key:"processChildItem",value:function(t,n){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(r.hasOwnProperty("single")&&document.querySelectorAll(".child-hidden-input").forEach((function(e){return e.remove()})),!1!==t.checked){var o=document.createElement("INPUT");o.setAttribute("name","purchase_orders[]"),o.setAttribute("value",t.dataset.value),o.setAttribute("class","child-hidden-input"),o.hidden=!0,n.append(o)}else{var c,i=document.querySelectorAll("input.child-hidden-input"),a=e(i);try{for(a.s();!(c=a.n()).done;){var l=c.value;l.value==t.dataset.value&&l.remove()}}catch(e){a.e(e)}finally{a.f()}}}},{key:"handle",value:function(){var t=this;this.parentElement.addEventListener("click",(function(){t.watchCheckboxes(t.parentElement)}));var n,r=e(document.querySelectorAll(".form-check-child"));try{var o=function(){var e=n.value;e.addEventListener("click",(function(){t.processChildItem(e,t.parentForm)}))};for(r.s();!(n=r.n()).done;)o()}catch(e){r.e(e)}finally{r.f()}}}],o&&n(r.prototype,o),c&&n(r,c),Object.defineProperty(r,"prototype",{writable:!1}),t}())).handle()})();

View File

@ -0,0 +1,9 @@
/**
* 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
*/

297796
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

295618
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More