Merge pull request #4977 from turbo124/v5-stable

Version Bump
This commit is contained in:
David Bomba 2021-02-24 07:48:40 +11:00 committed by GitHub
commit 9c174984cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 111846 additions and 110580 deletions

View File

@ -105,4 +105,4 @@ jobs:
- name: Run php-cs-fixer
run: |
vendor/bin/php-cs-fixer fix
vendor/bin/php-cs-fixer fix

View File

@ -1 +1 @@
5.1.8
5.1.9

View File

@ -136,7 +136,7 @@ class CompanySettings extends BaseSettings
public $tax_name3 = ''; //@TODO where do we use this?
public $tax_rate3 = 0; //@TODO where do we use this?
public $payment_type_id = '0'; //@TODO where do we use this?
public $invoice_fields = ''; //@TODO is this redundant, we store this in the custom_fields on the company?
// public $invoice_fields = ''; //@TODO is this redundant, we store this in the custom_fields on the company?
public $show_accept_invoice_terms = false; //@TODO ben to confirm
public $show_accept_quote_terms = false; //@TODO ben to confirm
@ -392,7 +392,7 @@ class CompanySettings extends BaseSettings
'invoice_number_pattern' => 'string',
'invoice_number_counter' => 'integer',
'invoice_design_id' => 'string',
'invoice_fields' => 'string',
// 'invoice_fields' => 'string',
'invoice_taxes' => 'int',
//'enabled_item_tax_rates' => 'int',
'invoice_footer' => 'string',

View File

@ -99,21 +99,21 @@ class Handler extends ExceptionHandler
private function validException($exception)
{
if (strpos($exception->getMessage(), 'file_put_contents') !== false) {
if (strpos($exception->getMessage(), 'file_put_contents') !== false)
return false;
}
if (strpos($exception->getMessage(), 'Permission denied') !== false) {
if (strpos($exception->getMessage(), 'Permission denied') !== false)
return false;
}
if (strpos($exception->getMessage(), 'flock()') !== false) {
if (strpos($exception->getMessage(), 'flock()') !== false)
return false;
}
if (strpos($exception->getMessage(), 'expects parameter 1 to be resource') !== false) {
if (strpos($exception->getMessage(), 'expects parameter 1 to be resource') !== false)
return false;
}
if (strpos($exception->getMessage(), 'fwrite()') !== false)
return false;
return true;
}

View File

@ -27,7 +27,8 @@ trait CustomValuer
public function valuerTax($custom_value, $has_custom_invoice_taxes)
{
if (isset($custom_value) && is_numeric($custom_value) && $has_custom_invoice_taxes === true) {
if (isset($custom_value) && is_numeric($custom_value) && $has_custom_invoice_taxes) {
return round($custom_value * ($this->invoice->tax_rate1 / 100), 2) + round($custom_value * ($this->invoice->tax_rate2 / 100), 2) + round($custom_value * ($this->invoice->tax_rate3 / 100), 2);
}

View File

@ -57,8 +57,8 @@ class InvoiceSum
{
$this->calculateLineItems()
->calculateDiscount()
->calculateCustomValues()
->calculateInvoiceTaxes()
->calculateCustomValues()
->setTaxMap()
->calculateTotals()
->calculateBalance()
@ -89,16 +89,17 @@ class InvoiceSum
private function calculateCustomValues()
{
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge1, $this->invoice->custom_surcharge_taxes1);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge1, $this->invoice->custom_surcharge_tax1);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge1);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge2, $this->invoice->custom_surcharge_taxes2);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge2, $this->invoice->custom_surcharge_tax2);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge2);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge3, $this->invoice->custom_surcharge_taxes3);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge3, $this->invoice->custom_surcharge_tax3);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge3);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge4, $this->invoice->custom_surcharge_taxes4);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge4, $this->invoice->custom_surcharge_tax4);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge4);
$this->total += $this->total_custom_values;
@ -108,24 +109,25 @@ class InvoiceSum
private function calculateInvoiceTaxes()
{
if ($this->invoice->tax_rate1 > 0) {
if (strlen($this->invoice->tax_name1) > 1) {
$tax = $this->taxer($this->total, $this->invoice->tax_rate1);
$this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax];
}
if ($this->invoice->tax_rate2 > 0) {
if (strlen($this->invoice->tax_name2) > 1) {
$tax = $this->taxer($this->total, $this->invoice->tax_rate2);
$this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax];
}
if ($this->invoice->tax_rate3 > 0) {
if (strlen($this->invoice->tax_name3) > 1) {
$tax = $this->taxer($this->total, $this->invoice->tax_rate3);
$this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax];
}
return $this;
}
@ -299,7 +301,7 @@ class InvoiceSum
}
public function getTaxMap()
{
{
return $this->tax_map;
}

View File

@ -89,16 +89,16 @@ class InvoiceSumInclusive
private function calculateCustomValues()
{
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge1, $this->invoice->custom_surcharge_taxes1);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge1, $this->invoice->custom_surcharge_tax1);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge1);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge2, $this->invoice->custom_surcharge_taxes2);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge2, $this->invoice->custom_surcharge_tax2);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge2);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge3, $this->invoice->custom_surcharge_taxes3);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge3, $this->invoice->custom_surcharge_tax3);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge3);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge4, $this->invoice->custom_surcharge_taxes4);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge4, $this->invoice->custom_surcharge_tax4);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge4);
$this->total += $this->total_custom_values;

View File

@ -54,11 +54,11 @@ class InvoiceController extends Controller
'invoice' => $invoice,
];
if ($request->query('mode') === 'portal') {
return $this->render('invoices.show', $data);
if ($request->query('mode') === 'fullscreen') {
return response()->file($invoice->pdf_file_path(null, 'path'));
}
return $this->render('invoices.show.fullscreen', $data);
return $this->render('invoices.show', $data);
}
/**

View File

@ -35,7 +35,7 @@ class QuoteController extends Controller
*
* @param ShowQuoteRequest $request
* @param Quote $quote
* @return Factory|View
* @return Factory|View|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function show(ShowQuoteRequest $request, Quote $quote)
{
@ -43,11 +43,11 @@ class QuoteController extends Controller
'quote' => $quote,
];
if ($request->query('mode') === 'portal') {
return $this->render('quotes.show', $data);
if ($request->query('mode') === 'fullscreen') {
return response()->file($quote->pdf_file_path(null, 'path'));
}
return $this->render('quotes.show.fullscreen', $data);
return $this->render('quotes.show', $data);
}
public function bulk(ProcessQuotesInBulkRequest $request)

View File

@ -0,0 +1,139 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers;
use App\Libraries\MultiDB;
use App\Libraries\OAuth\Providers\Google;
use Illuminate\Http\Request;
class ConnectedAccountController extends BaseController
{
public function __construct()
{
parent::__construct();
}
/**
* Connect an OAuth account to a regular email/password combination account
*
* @param Request $request
* @return User Refresh Feed.
*
*
* @OA\Post(
* path="/api/v1/connected_account",
* operationId="connected_account",
* tags={"connected_account"},
* summary="Connect an oauth user to an existing user",
* description="Refreshes the dataset",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(ref="#/components/parameters/include_static"),
* @OA\Parameter(ref="#/components/parameters/clear_cache"),
* @OA\Response(
* response=200,
* description="The Company User response",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/User"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function index(Request $request)
{
if ($request->input('provider') == 'google') {
return $this->handleGoogleOauth();
}
return response()
->json(['message' => 'Provider not supported'], 400)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
private function handleGoogleOauth()
{
$user = false;
$google = new Google();
$user = $google->getTokenResponse(request()->input('id_token'));
if (is_array($user)) {
$query = [
'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id'=> 'google',
];
/* Cannot allow duplicates! */
if ($existing_user = MultiDB::hasUser($query)) {
return response()
->json(['message' => 'User already exists in system.'], 401)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
}
if ($user) {
$client = new Google_Client();
$client->setClientId(config('ninja.auth.google.client_id'));
$client->setClientSecret(config('ninja.auth.google.client_secret'));
$client->setRedirectUri(config('ninja.app_url'));
$token = $client->authenticate(request()->input('server_auth_code'));
$refresh_token = '';
if (array_key_exists('refresh_token', $token)) {
$refresh_token = $token['refresh_token'];
}
$connected_account = [
'password' => '',
'email' => $google->harvestEmail($user),
'oauth_user_id' => $google->harvestSubField($user),
'oauth_user_token' => $token,
'oauth_user_refresh_token' => $refresh_token,
'oauth_provider_id' => 'google',
'email_verified_at' =>now()
];
auth()->user()->update($connected_account);
auth()->user()->email_verified_at = now();
auth()->user()->save();
//$ct = CompanyUser::whereUserId(auth()->user()->id);
return $this->listResponse(auth()->user());
}
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'));
}
}

View File

@ -1,4 +1,13 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers;

View File

@ -78,7 +78,6 @@
* @OA\Property(property="tax_name3", type="string", example="GST", description="The tax name"),
* @OA\Property(property="payment_type_id", type="string", example="1", description="The default payment type id"),
* @OA\Property(property="custom_fields", type="string", example="{}", description="JSON string of custom fields"),
* @OA\Property(property="invoice_fields", type="string", example="{}", description="JSON string of invoice fields"),
* @OA\Property(property="email_footer", type="string", example="A default email footer", description="The default email footer"),
* @OA\Property(property="email_sending_method", type="string", example="default", description="The email driver to use to send email, options include default, gmail"),
* @OA\Property(property="gmail_sending_user_id", type="string", example="F76sd34D", description="The hashed_id of the user account to send email from"),

View File

@ -49,6 +49,9 @@
* @OA\Property(property="custom_surcharge2", type="number", format="float", example="10.00", description="Second Custom Surcharge"),
* @OA\Property(property="custom_surcharge3", type="number", format="float", example="10.00", description="Third Custom Surcharge"),
* @OA\Property(property="custom_surcharge4", type="number", format="float", example="10.00", description="Fourth Custom Surcharge"),
* @OA\Property(property="custom_surcharge_taxes", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax1", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax2", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax3", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax4", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* )
*/

View File

@ -48,6 +48,9 @@
* @OA\Property(property="custom_surcharge2", type="number", format="float", example="10.00", description="Second Custom Surcharge"),
* @OA\Property(property="custom_surcharge3", type="number", format="float", example="10.00", description="Third Custom Surcharge"),
* @OA\Property(property="custom_surcharge4", type="number", format="float", example="10.00", description="Fourth Custom Surcharge"),
* @OA\Property(property="custom_surcharge_taxes", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax1", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax2", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax3", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax4", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* )
*/

View File

@ -48,6 +48,9 @@
* @OA\Property(property="custom_surcharge2", type="number", format="float", example="10.00", description="Second Custom Surcharge"),
* @OA\Property(property="custom_surcharge3", type="number", format="float", example="10.00", description="Third Custom Surcharge"),
* @OA\Property(property="custom_surcharge4", type="number", format="float", example="10.00", description="Fourth Custom Surcharge"),
* @OA\Property(property="custom_surcharge_taxes", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax1", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax2", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax3", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax4", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* )
*/

View File

@ -0,0 +1,156 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers;
use Illuminate\Http\Request;
/**
* Class PostMarkController.
*/
class PostMarkController extends BaseController
{
public function __construct()
{
}
/**
* Process Postmark Webhook.
*
*
* @OA\Post(
* path="/api/v1/postmark_webhook",
* operationId="postmarkWebhook",
* tags={"postmark"},
* summary="Processing webhooks from PostMark",
* description="Adds an credit to the system",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Response(
* response=200,
* description="Returns the saved credit object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Credit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function webhook(Request $request)
{
if($request->header('X-API-SECURITY') && $request->header('X-API-SECURITY') == config('postmark.secret'))
{
}
}
// {
// "RecordType": "Delivery",
// "ServerID": 23,
// "MessageStream": "outbound",
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "Recipient": "john@example.com",
// "Tag": "welcome-email",
// "DeliveredAt": "2021-02-21T16:34:52Z",
// "Details": "Test delivery webhook details",
// "Metadata": {
// "example": "value",
// "example_2": "value"
// }
// }
private function processDelivery($request)
{
}
// {
// "Metadata": {
// "example": "value",
// "example_2": "value"
// },
// "RecordType": "Bounce",
// "ID": 42,
// "Type": "HardBounce",
// "TypeCode": 1,
// "Name": "Hard bounce",
// "Tag": "Test",
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "ServerID": 1234,
// "MessageStream": "outbound",
// "Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).",
// "Details": "Test bounce details",
// "Email": "john@example.com",
// "From": "sender@example.com",
// "BouncedAt": "2021-02-21T16:34:52Z",
// "DumpAvailable": true,
// "Inactive": true,
// "CanActivate": true,
// "Subject": "Test subject",
// "Content": "Test content"
// }
private function processBounce($request)
{
}
// {
// "Metadata": {
// "example": "value",
// "example_2": "value"
// },
// "RecordType": "SpamComplaint",
// "ID": 42,
// "Type": "SpamComplaint",
// "TypeCode": 100001,
// "Name": "Spam complaint",
// "Tag": "Test",
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "ServerID": 1234,
// "MessageStream": "outbound",
// "Description": "The subscriber explicitly marked this message as spam.",
// "Details": "Test spam complaint details",
// "Email": "john@example.com",
// "From": "sender@example.com",
// "BouncedAt": "2021-02-21T16:34:52Z",
// "DumpAvailable": true,
// "Inactive": true,
// "CanActivate": false,
// "Subject": "Test subject",
// "Content": "Test content"
// }
private function processSpamComplaint($request)
{
}
}

View File

@ -406,9 +406,9 @@ class ProductController extends BaseController
*/
public function destroy(DestroyProductRequest $request, Product $product)
{
$product->delete();
$this->product_repo->delete($product);
return $this->itemResponse($product);
return $this->itemResponse($product->fresh());
}
/**

View File

@ -0,0 +1,63 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers;
use PragmaRX\Google2FA\Google2FA;
use Crypt;
class TwoFactorController extends BaseController
{
public function setupTwoFactor()
{
$user = auth()->user();
if ($user->google_2fa_secret)
return response()->json(['message' => '2FA already enabled'], 400);
elseif(! $user->phone)
return response()->json(['message' => ctrans('texts.set_phone_for_two_factor')], 400);
elseif(! $user->confirmed)
return response()->json(['message' => 'Please confirm your account first'], 400);
$google2fa = new Google2FA();
$secret = $google2fa->generateSecretKey();
$qr_code = $google2fa->getQRCodeGoogleUrl(
config('ninja.app_name')
$user->email,
$secret
);
$data = [
'secret' => $secret,
'qrCode' => $qrCode,
];
return response()->json(['data' => $data], 200);
}
public function enableTwoFactor()
{
$user = auth()->user();
$secret = request()->input('secret');
$oneTimePassword = request()->input('one_time_password');
if (! $secret || ! \Google2FA::verifyKey($secret, $oneTimePassword)) {
return response()->json('message' > ctrans('texts.invalid_one_time_password'));
} elseif (! $user->google_2fa_secret && $user->phone && $user->confirmed) {
$user->google_2fa_secret = encrypt($secret);
$user->save();
}
return response()->json(['message' => ctrans('texts.enabled_two_factor')], 200);
}
}

View File

@ -23,11 +23,16 @@ use App\Http\Requests\User\CreateUserRequest;
use App\Http\Requests\User\DestroyUserRequest;
use App\Http\Requests\User\DetachCompanyUserRequest;
use App\Http\Requests\User\EditUserRequest;
use App\Http\Requests\User\ReconfirmUserRequest;
use App\Http\Requests\User\ShowUserRequest;
use App\Http\Requests\User\StoreUserRequest;
use App\Http\Requests\User\UpdateUserRequest;
use App\Jobs\Company\CreateCompanyToken;
use App\Jobs\Mail\NinjaMailer;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\User\UserEmailChanged;
use App\Mail\Admin\VerifyUserObject;
use App\Models\CompanyUser;
use App\Models\User;
use App\Repositories\UserRepository;
@ -378,11 +383,12 @@ class UserController extends BaseController
$new_user = $this->user_repo->save($request->all(), $user);
$new_user = $user->fresh();
nlog($old_user);
if ($old_user_email != $new_email)
/* When changing email address we store the former email in case we need to rollback */
if ($old_user_email != $new_email) {
$user->last_confirmed_email_address = $old_user_email;
$user->save();
UserEmailChanged::dispatch($new_user, json_decode($old_user), auth()->user()->company());
}
if(
@ -684,4 +690,70 @@ class UserController extends BaseController
return response()->json(['message' => ctrans('texts.user_detached')], 200);
}
/**
* Detach an existing user to a company.
*
* @OA\Post(
* path="/api/v1/users/{user}/reconfirm",
* operationId="reconfirmUser",
* tags={"users"},
* summary="Reconfirm an existing user to a company",
* description="Reconfirm an existing user from a company",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="user",
* in="path",
* description="The user hashed_id",
* example="FD767dfd7",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Success response",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
* @param ReconfirmUserRequest $request
* @param User $user
* @return \Illuminate\Http\JsonResponse
*/
public function reconfirm(ReconfirmUserRequest $request, User $user)
{
$user->confirmation_code = $this->createDbHash($user->company()->db);
$user->save();
$nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer((new VerifyUserObject($user, $user->company()))->build());
$nmo->company = $user->company();
$nmo->to_user = $user;
$nmo->settings = $user->company->settings;
NinjaMailerJob::dispatch($nmo);
return response()->json(['message' => ctrans('texts.confirmation_resent')], 200);
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Livewire;
use Livewire\Component;
class PayNowDropdown extends Component
{
public $total;
public $methods;
public function mount(int $total)
{
$this->total = $total;
$this->methods = auth()->user()->client->service()->getPaymentMethods($total);
}
public function render()
{
return render('components.livewire.pay-now-dropdown');
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\User;
use App\Http\Requests\Request;
use App\Models\User;
class ReconfirmUserRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->Admin();
}
}

View File

@ -105,7 +105,7 @@ class EmailEntity implements ShouldQueue
MultiDB::setDB($this->company->db);
$nmo = new NinjaMailerObject;
$nmo->mailable = new TemplateEmail($this->email_entity_builder,$this->invitation->contact);
$nmo->mailable = new TemplateEmail($this->email_entity_builder,$this->invitation->contact, $this->invitation);
$nmo->company = $this->company;
$nmo->settings = $this->settings;
$nmo->to_user = $this->invitation->contact;

View File

@ -54,6 +54,7 @@ use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
class CSVImport implements ShouldQueue {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, CleanLineItems;
public $invoice;
@ -79,7 +80,9 @@ class CSVImport implements ShouldQueue {
$this->hash = $request['hash'];
$this->import_type = $request['import_type'];
$this->skip_header = $request['skip_header'] ?? null;
$this->column_map = $request['column_map'] ?? null;
$this->column_map =
! empty( $request['column_map'] ) ?
array_combine( array_keys( $request['column_map'] ), array_column( $request['column_map'], 'mapping' ) ) : null;
}
/**

View File

@ -142,7 +142,7 @@ class NinjaMailerJob implements ShouldQueue
$user = User::find($this->decodePrimaryKey($sending_user));
nlog("Sending via {$user->present()->name()}");
nlog("Sending via {$user->name()}");
$google = (new Google())->init();
$google->getClient()->setAccessToken(json_encode($user->oauth_user_token));
@ -164,7 +164,7 @@ class NinjaMailerJob implements ShouldQueue
$this->nmo
->mailable
->from($user->email, $user->present()->name())
->from($user->email, $user->name())
->withSwiftMessage(function ($message) use($token) {
$message->getHeaders()->addTextHeader('GmailToken', $token);
});

View File

@ -295,7 +295,7 @@ class SendReminders implements ShouldQueue
$invoice_item = new InvoiceItem;
$invoice_item->type_id = '5';
$invoice_item->product_key = trans('texts.fee');
$invoice_item->notes = ctrans('texts.late_fee_added', ['date' => $this->formatDate(now()->startOfDay(), $invoice->client->date_format())]);
$invoice_item->notes = ctrans('texts.late_fee_added', ['date' => $this->translateDate(now()->startOfDay(), $invoice->client->date_format(), $invoice->client->locale())]);
$invoice_item->quantity = 1;
$invoice_item->cost = $fee;

View File

@ -0,0 +1,58 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Listeners\Mail;
use App\Libraries\MultiDB;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Notification;
use Illuminate\Mail\Events\MessageSent;
class MailSentListener implements ShouldQueue
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle(MessageSent $event)
{
if(property_exists($event->message, 'invitation')){
MultiDB::setDb($event->message->invitation->company->db);
if($event->message->getHeaders()->get('x-pm-message-id')){
$postmark_id = $event->message->getHeaders()->get('x-pm-message-id')->getValue();
nlog($postmark_id);
$invitation = $event->message->invitation;
$invitation->message_id = $postmark_id;
$invitation->save();
}
}
}
}

View File

@ -50,21 +50,5 @@ class BouncedEmail extends Mailable
->text()
->subject($subject);
//todo
/*
//todo determine WHO is notified!! In this instance the _user_ is notified
Mail::to($invitation->user->email)
//->cc('')
//->bcc('')
->queue(new BouncedEmail($invitation));
return $this->from('x@gmail.com') //todo
->subject(ctrans('texts.confirmation_subject'))
->markdown('email.auth.verify', ['user' => $this->user])
->text('email.auth.verify_text');
*/
}
}

View File

@ -31,6 +31,8 @@ class BaseEmailEngine implements EngineInterface
public $text;
public $invitation;
public function setFooter($footer)
{
$this->footer = $footer;
@ -141,4 +143,15 @@ class BaseEmailEngine implements EngineInterface
public function build()
{
}
public function setInvitation($invitation)
{
$this->invitation = $invitation;
}
public function getInvitation()
{
return $this->invitation;
}
}

View File

@ -85,7 +85,8 @@ class CreditEmailEngine extends BaseEmailEngine
->setBody($body_template)
->setFooter("<a href='{$this->invitation->getLink()}'>".ctrans('texts.view_credit').'</a>')
->setViewLink($this->invitation->getLink())
->setViewText(ctrans('texts.view_credit'));
->setViewText(ctrans('texts.view_credit'))
->setInvitation($this->invitation);
if ($this->client->getSetting('pdf_email_attachment') !== false) {
$this->setAttachments(['path' => $this->credit->pdf_file_path(), 'name' => basename($this->credit->pdf_file_path())]);

View File

@ -94,7 +94,8 @@ class InvoiceEmailEngine extends BaseEmailEngine
->setBody($body_template)
->setFooter("<a href='{$this->invitation->getLink()}'>".ctrans('texts.view_invoice').'</a>')
->setViewLink($this->invitation->getLink())
->setViewText(ctrans('texts.view_invoice'));
->setViewText(ctrans('texts.view_invoice'))
->setInvitation($this->invitation);
if ($this->client->getSetting('pdf_email_attachment') !== false) {
$this->setAttachments([$this->invoice->pdf_file_path()]);

View File

@ -87,7 +87,7 @@ class PaymentEmailEngine extends BaseEmailEngine
$data['$entity'] = ['value' => '', 'label' => ctrans('texts.payment')];
$data['$payment.amount'] = ['value' => Number::formatMoney($this->payment->amount, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.amount')];
$data['$amount'] = &$data['$payment.amount'];
$data['$payment.date'] = ['value' => $this->formatDate($this->payment->date, $this->client->date_format()), 'label' => ctrans('texts.payment_date')];
$data['$payment.date'] = ['value' => $this->translateDate($this->payment->date, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.payment_date')];
$data['$transaction_reference'] = ['value' => $this->payment->transaction_reference, 'label' => ctrans('texts.transaction_reference')];
$data['$public_notes'] = ['value' => $this->payment->public_notes, 'label' => ctrans('texts.notes')];

View File

@ -85,7 +85,9 @@ class QuoteEmailEngine extends BaseEmailEngine
->setBody($body_template)
->setFooter("<a href='{$this->invitation->getLink()}'>".ctrans('texts.view_quote').'</a>')
->setViewLink($this->invitation->getLink())
->setViewText(ctrans('texts.view_quote'));
->setViewText(ctrans('texts.view_quote'))
->setInvitation($this->invitation);
if ($this->client->getSetting('pdf_email_attachment') !== false) {
// $this->setAttachments([$this->quote->pdf_file_path()]);

View File

@ -1,32 +0,0 @@
<?php
namespace App\Mail\Invoices;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class InvoiceWasPaid extends Mailable
{
// use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->from(config('mail.from.address'), config('mail.from.name'))->view('email.invoices.paid');
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace App\Mail\Quote;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class QuoteWasApproved extends Mailable
{
// use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->from(config('mail.from.address'), config('mail.from.name'))->view('email.quotes.approved');
}
}

View File

@ -27,13 +27,21 @@ class TemplateEmail extends Mailable
private $contact;
public function __construct($build_email, ClientContact $contact)
private $company;
private $invitation;
public function __construct($build_email, ClientContact $contact, $invitation = null)
{
$this->build_email = $build_email;
$this->contact = $contact;
$this->client = $contact->client;
$this->company = $contact->company;
$this->invitation = $invitation;
}
public function build()
@ -44,15 +52,13 @@ class TemplateEmail extends Mailable
$company = $this->client->company;
$this->from(config('mail.from.address'), config('mail.from.name'));
$this->from(config('mail.from.address'), $this->company->present()->name());
if (strlen($settings->reply_to_email) > 1) {
if (strlen($settings->reply_to_email) > 1)
$this->replyTo($settings->reply_to_email, $settings->reply_to_email);
}
if (strlen($settings->bcc_email) > 1) {
if (strlen($settings->bcc_email) > 1)
$this->bcc($settings->bcc_email, $settings->bcc_email);
}
$this->subject($this->build_email->getSubject())
->text('email.template.plain', [
@ -75,6 +81,7 @@ class TemplateEmail extends Mailable
])
->withSwiftMessage(function ($message) use($company){
$message->getHeaders()->addTextHeader('Tag', $company->company_key);
$message->invitation = $this->invitation;
});
//conditionally attach files

View File

@ -41,14 +41,11 @@ class Credit extends BaseModel
protected $presenter = CreditPresenter::class;
protected $fillable = [
'assigned_user_id',
'project_id',
'number',
'discount',
'po_number',
'date',
'due_date',
'partial_due_date',
'terms',
'public_notes',
'private_notes',
@ -59,8 +56,9 @@ class Credit extends BaseModel
'tax_name3',
'tax_rate3',
'is_amount_discount',
'footer',
'partial',
'partial_due_date',
'project_id',
'custom_value1',
'custom_value2',
'custom_value3',
@ -68,7 +66,16 @@ class Credit extends BaseModel
'line_items',
'client_id',
'footer',
'custom_surcharge1',
'custom_surcharge2',
'custom_surcharge3',
'custom_surcharge4',
'custom_surcharge_tax1',
'custom_surcharge_tax2',
'custom_surcharge_tax3',
'custom_surcharge_tax4',
'design_id',
'assigned_user_id',
'exchange_rate',
];

View File

@ -7,6 +7,9 @@ namespace App\Models;
*/
class DateFormat extends StaticModel
{
protected $fillable = ['translated_format'];
public static $days_of_the_week = [
0 => 'sunday',
1 => 'monday',

View File

@ -102,6 +102,10 @@ class Invoice extends BaseModel
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
'custom_surcharge_tax1' => 'bool',
'custom_surcharge_tax2' => 'bool',
'custom_surcharge_tax3' => 'bool',
'custom_surcharge_tax4' => 'bool',
];
protected $with = [];
@ -146,6 +150,16 @@ class Invoice extends BaseModel
return $this->belongsTo(Company::class);
}
public function project()
{
return $this->belongsTo(Project::class);
}
public function design()
{
return $this->belongsTo(Design::class);
}
public function user()
{
return $this->belongsTo(User::class)->withTrashed();
@ -367,13 +381,13 @@ class Invoice extends BaseModel
return $invoice_calc->build();
}
public function pdf_file_path($invitation = null)
public function pdf_file_path($invitation = null, string $type = 'url')
{
if (! $invitation) {
$invitation = $this->invitations->first();
}
$storage_path = Storage::url($this->client->invoice_filepath().$this->number.'.pdf');
$storage_path = Storage::$type($this->client->invoice_filepath().$this->number.'.pdf');
if (! Storage::exists($this->client->invoice_filepath().$this->number.'.pdf')) {
event(new InvoiceWasUpdated($this, $this->company, Ninja::eventVars()));

View File

@ -42,7 +42,6 @@ class Quote extends BaseModel
protected $touches = [];
protected $fillable = [
'assigned_user_id',
'number',
'discount',
'po_number',
@ -51,7 +50,6 @@ class Quote extends BaseModel
'terms',
'public_notes',
'private_notes',
'project_id',
'tax_name1',
'tax_rate1',
'tax_name2',
@ -61,6 +59,7 @@ class Quote extends BaseModel
'is_amount_discount',
'partial',
'partial_due_date',
'project_id',
'custom_value1',
'custom_value2',
'custom_value3',
@ -68,7 +67,16 @@ class Quote extends BaseModel
'line_items',
'client_id',
'footer',
'custom_surcharge1',
'custom_surcharge2',
'custom_surcharge3',
'custom_surcharge4',
'custom_surcharge_tax1',
'custom_surcharge_tax2',
'custom_surcharge_tax3',
'custom_surcharge_tax4',
'design_id',
'assigned_user_id',
'exchange_rate',
];
@ -187,13 +195,13 @@ class Quote extends BaseModel
return new QuoteService($this);
}
public function pdf_file_path($invitation = null)
public function pdf_file_path($invitation = null, string $type = 'url')
{
if (! $invitation) {
$invitation = $this->invitations->where('client_contact_id', $this->client->primary_contact()->first()->id)->first();
}
$storage_path = Storage::url($this->client->quote_filepath().$this->number.'.pdf');
$storage_path = Storage::$type($this->client->quote_filepath().$this->number.'.pdf');
if (Storage::exists($this->client->quote_filepath().$this->number.'.pdf')) {
return $storage_path;

View File

@ -74,7 +74,6 @@ class RecurringInvoice extends BaseModel
'due_date',
'due_date_days',
'line_items',
'settings',
'footer',
'public_notes',
'private_notes',
@ -97,6 +96,17 @@ class RecurringInvoice extends BaseModel
'auto_bill',
'auto_bill_enabled',
'design_id',
'custom_surcharge1',
'custom_surcharge2',
'custom_surcharge3',
'custom_surcharge4',
'custom_surcharge_tax1',
'custom_surcharge_tax2',
'custom_surcharge_tax3',
'custom_surcharge_tax4',
'design_id',
'assigned_user_id',
'exchange_rate',
];
protected $casts = [

View File

@ -103,6 +103,12 @@ class User extends Authenticatable implements MustVerifyEmail
'deleted_at' => 'timestamp',
];
public function name()
{
return $this->first_name . ' ' . $this->last_name;
}
public function getEntityType()
{
return self::class;

View File

@ -136,6 +136,7 @@ use App\Listeners\Invoice\InvoiceRestoredActivity;
use App\Listeners\Invoice\InvoiceReversedActivity;
use App\Listeners\Invoice\InvoiceViewedActivity;
use App\Listeners\Invoice\UpdateInvoiceActivity;
use App\Listeners\Mail\MailSentListener;
use App\Listeners\Misc\InvitationViewedListener;
use App\Listeners\Payment\PaymentEmailFailureActivity;
use App\Listeners\Payment\PaymentEmailedActivity;
@ -157,6 +158,8 @@ use App\Listeners\User\RestoredUserActivity;
use App\Listeners\User\UpdateUserLastLogin;
use App\Listeners\User\UpdatedUserActivity;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Mail\Events\MessageSending;
use Illuminate\Mail\Events\MessageSent;
class EventServiceProvider extends ServiceProvider
{
@ -166,6 +169,11 @@ class EventServiceProvider extends ServiceProvider
* @var array
*/
protected $listen = [
MessageSending::class =>[
],
MessageSent::class => [
MailSentListener::class,
],
UserWasCreated::class => [
CreatedUserActivity::class,
SendVerificationNotification::class,

View File

@ -3,8 +3,10 @@
namespace App\Providers;
use App\Helpers\Mail\GmailTransportManager;
use Coconuts\Mail\PostmarkTransport;
use Illuminate\Mail\MailServiceProvider as MailProvider;
use Illuminate\Mail\TransportManager;
use GuzzleHttp\Client as HttpClient;
class MailServiceProvider extends MailProvider
{
@ -24,7 +26,18 @@ class MailServiceProvider extends MailProvider
$this->app->bind('mailer', function ($app) {
return $app->make('mail.manager')->mailer();
});
}
$this->app['mail.manager']->extend('postmark', function () {
return new PostmarkTransport(
$this->guzzle(config('postmark.guzzle', [])),
config('postmark.secret', config('services.postmark.secret'))
);
});
}
protected function guzzle(array $config): HttpClient
{
return new HttpClient($config);
}
}

View File

@ -199,6 +199,12 @@ class BaseRepository
unset($tmp_data['client_contacts']);
$model->fill($tmp_data);
$model->custom_surcharge_tax1 = $client->company->custom_surcharge_taxes1;
$model->custom_surcharge_tax2 = $client->company->custom_surcharge_taxes2;
$model->custom_surcharge_tax3 = $client->company->custom_surcharge_taxes3;
$model->custom_surcharge_tax4 = $client->company->custom_surcharge_taxes4;
$model->save();
/* Model now persisted, now lets do some child tasks */
@ -286,7 +292,7 @@ class BaseRepository
$model = $model->service()->applyNumber()->save();
/* Update product details if necessary */
if ($model->company->update_products !== false)
if ($model->company->update_products)
UpdateOrCreateProduct::dispatch($model->line_items, $model, $model->company);
/* Perform model specific tasks */

View File

@ -58,6 +58,7 @@ class UserTransformer extends EntityTransformer
'custom_value3' => $user->custom_value3 ?: '',
'custom_value4' => $user->custom_value4 ?: '',
'oauth_provider_id' => (string) $user->oauth_provider_id,
'last_confirmed_email_address' => (string) $user->last_confirmed_email_address ?: '',
];
}

View File

@ -49,7 +49,7 @@ class Helpers
*
* @return null|string
*/
public function formatCustomFieldValue($custom_fields = null, $field, $value, Client $client = null): ?string
public function formatCustomFieldValue($custom_fields, $field, $value, Client $client = null): ?string
{
$custom_field = '';
@ -64,7 +64,7 @@ class Helpers
switch ($custom_field) {
case 'date':
return is_null($client) ? $value : $this->formatDate($value, $client->date_format());
return is_null($client) ? $value : $this->translateDate($value, $client->date_format(), $client->locale());
break;
case 'switch':
@ -84,7 +84,7 @@ class Helpers
*
* @return string
*/
public function makeCustomField($custom_fields = null, $field): string
public function makeCustomField($custom_fields, $field): string
{
if ($custom_fields && property_exists($custom_fields, $field)) {
$custom_field = $custom_fields->{$field};

View File

@ -105,15 +105,15 @@ class HtmlEngine
$data['$total_tax_values'] = ['value' => $this->totalTaxValues(), 'label' => ctrans('texts.taxes')];
$data['$line_tax_labels'] = ['value' => $this->lineTaxLabels(), 'label' => ctrans('texts.taxes')];
$data['$line_tax_values'] = ['value' => $this->lineTaxValues(), 'label' => ctrans('texts.taxes')];
$data['$date'] = ['value' => $this->formatDate($this->entity->date, $this->entity->client->date_format()) ?: '&nbsp;', 'label' => ctrans('texts.date')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.date')];
$data['$invoice.date'] = &$data['$date'];
$data['$due_date'] = ['value' => $this->formatDate($this->entity->due_date, $this->entity->client->date_format()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
$data['$payment_due'] = ['value' => $this->formatDate($this->entity->due_date, $this->entity->client->date_format()) ?: '&nbsp;', 'label' => ctrans('texts.payment_due')];
$data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
$data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.payment_due')];
$data['$invoice.due_date'] = &$data['$due_date'];
$data['$invoice.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.invoice_number')];
$data['$invoice.po_number'] = ['value' => $this->entity->po_number ?: '&nbsp;', 'label' => ctrans('texts.po_number')];
$data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->entity->client->date_format()), 'label' => ctrans('texts.date')];
$data['$entity.datetime'] = ['value' => $this->formatDatetime($this->entity->created_at, $this->entity->client->date_format(), $this->entity->client->locale()), 'label' => ctrans('texts.date')];
$data['$invoice.datetime'] = &$data['$entity.datetime'];
$data['$quote.datetime'] = &$data['$entity.datetime'];
$data['$credit.datetime'] = &$data['$entity.datetime'];
@ -125,6 +125,9 @@ class HtmlEngine
$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_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')];
if($this->entity->project()->exists())
$data['$project.name'] = ['value' => $this->entity->project->name, 'label' => ctrans('texts.project_name')];
}
if ($this->entity_string == 'quote') {
@ -154,8 +157,11 @@ class HtmlEngine
if ($this->entity->partial > 0) {
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.partial_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')];
} else {
$data['$balance_due'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.balance_due')];
$data['$balance_due_raw'] = ['value' => $this->entity->balance, 'label' => ctrans('texts.balance_due')];
}
$data['$quote.balance_due'] = $data['$balance_due'];
@ -174,7 +180,7 @@ class HtmlEngine
$data['$credit.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.credit_number')];
$data['$credit.total'] = &$data['$credit.total'];
$data['$credit.po_number'] = &$data['$invoice.po_number'];
$data['$credit.date'] = ['value' => $this->formatDate($this->entity->date, $this->entity->client->date_format()), 'label' => ctrans('texts.credit_date')];
$data['$credit.date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()), 'label' => ctrans('texts.credit_date')];
$data['$balance'] = ['value' => Number::formatMoney($this->entity_calc->getBalance(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.balance')];
$data['$credit.balance'] = &$data['$balance'];
@ -186,20 +192,20 @@ class HtmlEngine
$data['$invoice.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')];
$data['$invoice.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')];
$data['$invoice.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')];
$data['$invoice.public_notes'] = ['value' => nl2br($this->entity->public_notes) ?: '&nbsp;', 'label' => ctrans('texts.public_notes')];
$data['$invoice.public_notes'] = ['value' => nl2br($this->entity->public_notes) ?: '', 'label' => ctrans('texts.public_notes')];
$data['$entity.public_notes'] = &$data['$invoice.public_notes'];
$data['$public_notes'] = &$data['$invoice.public_notes'];
$data['$entity_issued_to'] = ['value' => '', 'label' => ctrans("texts.{$this->entity_string}_issued_to")];
$data['$your_entity'] = ['value' => '', 'label' => ctrans("texts.your_{$this->entity_string}")];
$data['$quote.date'] = ['value' => $this->formatDate($this->entity->date, $this->entity->client->date_format()) ?: '&nbsp;', 'label' => ctrans('texts.quote_date')];
$data['$quote.date'] = ['value' => $this->translateDate($this->entity->date, $this->entity->client->date_format(), $this->entity->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.quote_date')];
$data['$quote.number'] = ['value' => $this->entity->number ?: '&nbsp;', 'label' => ctrans('texts.quote_number')];
$data['$quote.po_number'] = &$data['$invoice.po_number'];
$data['$quote.quote_number'] = &$data['$quote.number'];
$data['$quote_no'] = &$data['$quote.number'];
$data['$quote.quote_no'] = &$data['$quote.number'];
$data['$quote.valid_until'] = ['value' => $this->formatDate($this->entity->due_date, $this->client->date_format()), 'label' => ctrans('texts.valid_until')];
$data['$quote.valid_until'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->entity->client->locale()), 'label' => ctrans('texts.valid_until')];
$data['$credit_amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_amount')];
$data['$credit_balance'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: '&nbsp;', 'label' => ctrans('texts.credit_balance')];
@ -287,10 +293,10 @@ class HtmlEngine
$data['$company3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company3', $this->settings->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company3')];
$data['$company4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company4', $this->settings->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company4')];
$data['$custom_surcharge1'] = ['value' => $this->entity->custom_surcharge1 ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'custom_surcharge1')];
$data['$custom_surcharge2'] = ['value' => $this->entity->custom_surcharge2 ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'custom_surcharge2')];
$data['$custom_surcharge3'] = ['value' => $this->entity->custom_surcharge3 ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'custom_surcharge3')];
$data['$custom_surcharge4'] = ['value' => $this->entity->custom_surcharge4 ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'custom_surcharge4')];
$data['$custom_surcharge1'] = ['value' => Number::formatMoney($this->entity->custom_surcharge1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'surcharge1')];
$data['$custom_surcharge2'] = ['value' => Number::formatMoney($this->entity->custom_surcharge2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'surcharge2')];
$data['$custom_surcharge3'] = ['value' => Number::formatMoney($this->entity->custom_surcharge3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'surcharge3')];
$data['$custom_surcharge4'] = ['value' => Number::formatMoney($this->entity->custom_surcharge4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'surcharge4')];
$data['$product.item'] = ['value' => '', 'label' => ctrans('texts.item')];
$data['$product.date'] = ['value' => '', 'label' => ctrans('texts.date')];
@ -355,8 +361,6 @@ class HtmlEngine
$arrKeysLength = array_map('strlen', array_keys($data));
array_multisort($arrKeysLength, SORT_DESC, $data);
//info(print_r($data,1));
return $data;
}

View File

@ -169,6 +169,9 @@ trait CompanySettingsSaver
if (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter') {
$value = 'integer';
if($key == 'gmail_sending_user_id')
$value = 'string';
if (! property_exists($settings, $key)) {
continue;
} elseif ($this->checkAttribute($value, $settings->{$key})) {
@ -218,12 +221,14 @@ trait CompanySettingsSaver
case 'int':
case 'integer':
return ctype_digit(strval(abs($value)));
// return is_int($value) || ctype_digit(strval(abs($value)));
case 'real':
case 'float':
case 'double':
return is_float($value) || is_numeric(strval($value));
case 'string':
return method_exists($value, '__toString') || is_null($value) || is_string($value);
//return is_null($value) || is_string($value);
case 'bool':
case 'boolean':
return is_bool($value) || (int) filter_var($value, FILTER_VALIDATE_BOOLEAN);

View File

@ -60,9 +60,6 @@ trait MakesDates
if (!isset($date)) {
return '';
}
// if (!$date || strlen($date) < 1) {
// return '';
// }
if (is_string($date)) {
$date = $this->convertToDateObject($date);
@ -99,4 +96,13 @@ trait MakesDates
$dt->setTimezone(new DateTimeZone('UTC'));
return $dt;
}
}
public function translateDate($date, $format, $locale)
{
Carbon::setLocale($locale);
return Carbon::parse($date)->translatedFormat($format);
}
}

View File

@ -48,6 +48,10 @@ trait SettingsSaver
/*Separate loop if it is a _id field which is an integer cast as a string*/
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter') {
$value = 'integer';
if($key == 'gmail_sending_user_id')
$value = 'string';
if (! property_exists($settings, $key)) {
continue;
} elseif (! $this->checkAttribute($value, $settings->{$key})) {

View File

@ -30,19 +30,21 @@
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"asgrim/ofxparser": "^1.2",
"authorizenet/authorizenet": "^2.0",
"bacon/bacon-qr-code": "^2.0",
"beganovich/snappdf": "^1.0",
"checkout/checkout-sdk-php": "^1.0",
"cleverit/ubl_invoice": "^1.3",
"coconutcraig/laravel-postmark": "^2.10",
"composer/composer": "^2",
"czproject/git-php": "^3.17",
"dacastro4/laravel-gmail": "dev-master",
"doctrine/dbal": "^2.10",
"fideloper/proxy": "^4.2",
"fzaninotto/faker": "^1.4",
"google/apiclient": "^2.7",
"guzzlehttp/guzzle": "^7.0.1",
"hashids/hashids": "^3.0",
"hashids/hashids": "^4.0",
"intervention/image": "^2.5",
"laracasts/presenter": "^0.2.1",
"laravel/framework": "^8.0",
@ -59,11 +61,11 @@
"maennchen/zipstream-php": "^1.2",
"nwidart/laravel-modules": "^8.0",
"omnipay/paypal": "^3.0",
"pragmarx/google2fa": "^8.0",
"predis/predis": "^1.1",
"sentry/sentry-laravel": "^2",
"stripe/stripe-php": "^7.50",
"turbo124/beacon": "^1",
"turbo124/laravel-gmail": "^5.0",
"webpatser/laravel-countries": "dev-master#75992ad",
"wildbit/swiftmailer-postmark": "^3.3"
},
@ -89,7 +91,6 @@
"Database\\Seeders\\": "database/seeders/"
},
"files": [
"app/Libraries/OFX.php"
]
},
"autoload-dev": {

1102
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', ''),
'app_version' => '5.1.8',
'app_version' => '5.1.9',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),

30
config/postmark.php Normal file
View File

@ -0,0 +1,30 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Postmark credentials
|--------------------------------------------------------------------------
|
| Here you may provide your Postmark server API token.
|
*/
'secret' => env('POSTMARK_SECRET'),
/*
|--------------------------------------------------------------------------
| Guzzle options
|--------------------------------------------------------------------------
|
| Under the hood we use Guzzle to make API calls to Postmark.
| Here you may provide any request options for Guzzle.
|
*/
'guzzle' => [
'timeout' => 10,
'connect_timeout' => 10,
],
];

View File

@ -36,9 +36,6 @@ return [
'gmail' => [
'token' => '',
],
'postmark' => [
'token' => env('POSTMARK_API_TOKEN', ''),
],
'stripe' => [
'model' => App\Models\User::class,
'key' => env('STRIPE_KEY'),

View File

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

View File

@ -80,4 +80,4 @@ class DateFormatsSeeder extends Seeder
}
}
}
}
}

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"main.dart.js": "5e63c564cc944e8930bd80a70ea4e156",
"main.dart.js": "3720742fd85b1fbea07bcf1bd87689c0",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"favicon.ico": "51636d3a390451561744c42188ccd628",
@ -29,7 +29,7 @@ const RESOURCES = {
"assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3",
"assets/assets/images/logo.png": "090f69e23311a4b6d851b3880ae52541",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "3e722fd57a6db80ee119f0e2c230ccff",
"version.json": "1cca74946fc6f0a80171ac6667e8719a",
"version.json": "c71c432fdc63e809b2f63fcc64edd8cd",
"manifest.json": "77215c1737c7639764e64a192be2f7b8",
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"/": "23224b5e03519aaa87594403d54412cf"

219926
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
"/css/app.css": "/css/app.css?id=1ea5400f7ae7b45f050c",
"/css/app.css": "/css/app.css?id=58736e43b16ddde82ba9",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
"/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=206d7de4ac97612980ff",

View File

@ -1 +1 @@
{"app_name":"invoiceninja_flutter","version":"5.0.42","build_number":"42"}
{"app_name":"invoiceninja_flutter","version":"5.0.43","build_number":"43"}

View File

@ -8,7 +8,7 @@
<p>{{ $greeting }}</p>
@endif
<p>{{ $title }}</p>
<h2>{{ $title }}</h2>
<p>{{ $message }}</p>

View File

@ -19,7 +19,7 @@
}
.primary-color-bg {
background-color: var(--primary-color);
background-color: {{ isset($settings) ? $settings->primary_color : '#4caf50' }};
}
#email-content h1, h2, h3, h4 {
@ -33,11 +33,11 @@
display: block;
color: {{ $design == 'light' ? 'black' : 'white' }};
padding-bottom: 20px;
padding-top: 20px;
/*padding-top: 20px;*/
}
.button {
background-color: var(--primary-color);
background-color: {{ isset($settings) ? $settings->primary_color : '#4caf50' }};
color: white;
padding: 10px 16px;
text-decoration: none;

View File

@ -0,0 +1,45 @@
<div>
@unless(count($methods) == 0)
<div x-data="{ open: false }" @keydown.window.escape="open = false" @click.away="open = false"
class="relative inline-block text-left" data-cy="payment-methods-dropdown">
<div>
<div class="rounded-md shadow-sm">
<button data-cy="pay-now-dropdown" @click="open = !open" type="button"
class="inline-flex justify-center w-full px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800">
{{ ctrans('texts.pay_now') }}
<svg class="w-5 h-5 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"/>
</svg>
</button>
</div>
</div>
<div x-show="open" class="absolute right-0 w-56 mt-2 origin-top-right rounded-md shadow-lg">
<div class="bg-white rounded-md shadow-xs">
<div class="py-1">
@foreach($methods as $index => $method)
@if($method['label'] == 'Custom')
<a href="#" @click="{ open = false }" data-cy="pay-with-custom"
data-company-gateway-id="{{ $method['company_gateway_id'] }}"
data-gateway-type-id="{{ $method['gateway_type_id'] }}"
class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
data-cy="payment-method">
{{ \App\Models\CompanyGateway::find($method['company_gateway_id'])->firstOrFail()->getConfigField('name') }}
</a>
@elseif($total > 0)
<a href="#" @click="{ open = false }" data-cy="pay-with-{{ $index }}"
data-company-gateway-id="{{ $method['company_gateway_id'] }}"
data-gateway-type-id="{{ $method['gateway_type_id'] }}"
class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
data-cy="payment-method">
{{ $method['label'] }}
</a>
@endif
@endforeach
</div>
</div>
</div>
</div>
@endunless
</div>

View File

@ -19,38 +19,7 @@
<div class="col-span-6 md:col-start-2 md:col-span-4">
<div class="flex justify-end">
<div class="flex justify-end mb-2">
<!-- Pay now button -->
@if(count($payment_methods) > 0)
<div x-data="{ open: false }" @keydown.window.escape="open = false" @click.away="open = false" class="relative inline-block text-left" data-cy="payment-methods-dropdown">
<div>
<div class="rounded-md shadow-sm">
<button data-cy="pay-now-dropdown" @click="open = !open" type="button" class="inline-flex justify-center w-full px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800">
{{ ctrans('texts.pay_now') }}
<svg class="w-5 h-5 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
<div x-show="open" class="absolute right-0 w-56 mt-2 origin-top-right rounded-md shadow-lg">
<div class="bg-white rounded-md shadow-xs">
<div class="py-1">
@foreach($payment_methods as $index => $payment_method)
@if($payment_method['label'] == 'Custom')
<a href="#" @click="{ open = false }" data-cy="pay-with-custom" data-company-gateway-id="{{ $payment_method['company_gateway_id'] }}" data-gateway-type-id="{{ $payment_method['gateway_type_id'] }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" data-cy="payment-method">
{{ \App\Models\CompanyGateway::find($payment_method['company_gateway_id'])->firstOrFail()->getConfigField('name') }}
</a>
@elseif($total > 0)
<a href="#" @click="{ open = false }" data-cy="pay-with-{{ $index }}" data-company-gateway-id="{{ $payment_method['company_gateway_id'] }}" data-gateway-type-id="{{ $payment_method['gateway_type_id'] }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" data-cy="payment-method">
{{ $payment_method['label'] }}
</a>
@endif
@endforeach
</div>
</div>
</div>
</div>
@endif
@livewire('pay-now-dropdown', ['total' => $total])
</div>
</div>

View File

@ -3,7 +3,10 @@
@push('head')
<meta name="pdf-url" content="{{ $invoice->pdf_file_path() }}">
<meta name="show-invoice-terms" content="{{ $settings->show_accept_invoice_terms ? true : false }}">
<meta name="require-invoice-signature" content="{{ $settings->require_invoice_signature ? true : false }}">
<script src="{{ asset('js/vendor/pdf.js/pdf.min.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/signature_pad@2.3.2/dist/signature_pad.min.js"></script>
@endpush
@section('body')
@ -15,19 +18,30 @@
@endif
@if($invoice->isPayable())
<form action="{{ route('client.invoices.bulk') }}" method="post">
<form action="{{ ($settings->client_portal_allow_under_payment || $settings->client_portal_allow_over_payment) ? route('client.invoices.bulk') : route('client.payments.process') }}" method="post" id="payment-form">
@csrf
<input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}">
<input type="hidden" name="action" value="payment">
<input type="hidden" name="company_gateway_id" id="company_gateway_id">
<input type="hidden" name="payment_method_id" id="payment_method_id">
<input type="hidden" name="signature">
<input type="hidden" name="payable_invoices[0][amount]" value="{{ $invoice->partial > 0 ? \App\Utils\Number::formatValue($invoice->partial, $invoice->client->currency()) : \App\Utils\Number::formatValue($invoice->balance, $invoice->client->currency()) }}">
<input type="hidden" name="payable_invoices[0][invoice_id]" value="{{ $invoice->hashed_id }}">
<div class="bg-white shadow sm:rounded-lg mb-4" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoice_number_placeholder', ['invoice' => $invoice->number])}} - {{ ctrans('texts.unpaid') }}
{{ ctrans('texts.invoice_number_placeholder', ['invoice' => $invoice->number])}}
- {{ ctrans('texts.unpaid') }}
</h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p translate>
{{ ctrans('texts.invoice_still_unpaid') }}
<!-- This invoice is still not paid. Click the button to complete the payment. -->
{{ ctrans('texts.invoice_still_unpaid') }}
<!-- This invoice is still not paid. Click the button to complete the payment. -->
</p>
</div>
</div>
@ -35,7 +49,12 @@
<div class="inline-flex rounded-md shadow-sm">
<input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}">
<input type="hidden" name="action" value="payment">
<button class="button button-primary bg-primary">{{ ctrans('texts.pay_now') }}</button>
@if($settings->client_portal_allow_under_payment || $settings->client_portal_allow_over_payment)
<button class="button button-primary bg-primary">{{ ctrans('texts.pay_now') }}</button>
@else
@livewire('pay-now-dropdown', ['total' => $invoice->partial > 0 ? $invoice->partial : $invoice->balance])
@endif
</div>
</div>
</div>
@ -43,18 +62,18 @@
</div>
</form>
@else
<div class="bg-white shadow sm:rounded-lg mb-4">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoice_number_placeholder', ['invoice' => $invoice->number])}}
- {{ ctrans('texts.paid') }}
</h3>
<div class="bg-white shadow sm:rounded-lg mb-4">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoice_number_placeholder', ['invoice' => $invoice->number])}}
- {{ ctrans('texts.paid') }}
</h3>
</div>
</div>
</div>
</div>
</div>
</div>
@endif
@if($invoice->documents->count() > 0)
@ -90,57 +109,88 @@
<div class="flex items-center justify-between mt-4">
<section class="flex items-center">
<div class="items-center" style="display: none" id="pagination-button-container">
<button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300" id="previous-page-button" title="Previous page">
<button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300"
id="previous-page-button" title="Previous page">
<svg class="w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
</button>
<button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300" id="next-page-button" title="Next page">
<button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300"
id="next-page-button" title="Next page">
<svg class="w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</button>
</div>
<span class="text-sm text-gray-700 ml-2">{{ ctrans('texts.page') }}:
<span class="text-sm text-gray-700 ml-2 lg:hidden">{{ ctrans('texts.page') }}:
<span id="current-page-container"></span>
<span>{{ strtolower(ctrans('texts.of')) }}</span>
<span id="total-page-container"></span>
</span>
</section>
<section class="flex items-center space-x-1">
<div class="flex items-center mr-4 space-x-1">
<div class="flex items-center mr-4 space-x-1 lg:hidden">
<span class="text-gray-600 mr-2" id="zoom-level">100%</span>
<a href="#" id="zoom-in">
<svg class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600 cursor-pointer" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
<svg class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600 cursor-pointer"
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="11" y1="8" x2="11" y2="14"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
</a>
<a href="#" id="zoom-out">
<svg class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600 cursor-pointer" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
<svg class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600 cursor-pointer"
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
</a>
</div>
<div x-data="{ open: false }" @keydown.escape="open = false" @click.away="open = false" class="relative inline-block text-left">
<div x-data="{ open: false }" @keydown.escape="open = false" @click.away="open = false"
class="relative inline-block text-left">
<div>
<button @click="open = !open" class="flex items-center text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
</svg>
<button @click="open = !open"
class="flex items-center text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path
d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"/>
</svg>
</button>
</div>
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
<div x-show="open" x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
<div class="rounded-md bg-white shadow-xs">
<div class="py-1">
<a target="_blank" href="?mode=fullscreen" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ ctrans('texts.open_in_new_tab') }}</a>
</div>
<div class="py-1">
<a target="_blank" href="?mode=fullscreen"
class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ ctrans('texts.open_in_new_tab') }}</a>
</div>
</div>
</div>
</div>
</section>
</div>
<iframe src="{{ $invoice->pdf_file_path() }}" class="h-screen w-full border-0 sm:hidden lg:block mt-4"></iframe>
<div class="flex justify-center">
<canvas id="pdf-placeholder" class="shadow rounded-lg bg-white mt-4 p-4"></canvas>
<canvas id="pdf-placeholder" class="shadow rounded-lg bg-white lg:hidden mt-4 p-4"></canvas>
</div>
@include('portal.ninja2020.invoices.includes.terms', ['entities' => [$invoice], 'entity_type' => ctrans('texts.invoice')])
@include('portal.ninja2020.invoices.includes.signature')
@endsection
@section('footer')
<script src="{{ asset('js/clients/shared/pdf.js') }}"></script>
<script src="{{ asset('js/clients/invoices/payment.js') }}"></script>
@endsection

View File

@ -1,57 +0,0 @@
@extends('portal.ninja2020.layout.clean', ['custom_body_class' => 'overflow-y-hidden'])
@section('meta_title', ctrans('texts.view_invoice'))
@section('body')
@if($invoice->isPayable())
<form action="{{ route('client.invoices.bulk') }}" method="post">
@csrf
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoice_number_placeholder', ['invoice' => $invoice->number])}}
- {{ ctrans('texts.unpaid') }}
</h3>
<div class="mt-2 max-w-xl text-sm leading-5 text-gray-500">
<p translate>
{{ ctrans('texts.invoice_still_unpaid') }}
<!-- This invoice is still not paid. Click the button to complete the payment. -->
</p>
</div>
</div>
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
<a href="{{ route('client.invoice.show', $invoice->hashed_id) }}?mode=portal"
class="mr-4 text-primary">
&#8592; {{ ctrans('texts.client_portal') }}
</a>
<div class="inline-flex rounded-md shadow-sm">
<input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}">
<input type="hidden" name="action" value="payment">
<button class="button button-primary bg-primary">{{ ctrans('texts.pay_now') }}</button>
</div>
</div>
</div>
</div>
</div>
</form>
@else
<div class="bg-white shadow sm:rounded-lg mb-4">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{{ ctrans('texts.invoice_number_placeholder', ['invoice' => $invoice->number])}}
- {{ ctrans('texts.paid') }}
</h3>
<a href="{{ route('client.invoice.show', $invoice->hashed_id) }}?mode=portal"
class="mr-4 text-primary">
&#8592; {{ ctrans('texts.client_portal') }}
</a>
</div>
</div>
</div>
@endif
<iframe src="{{ $invoice->pdf_file_path() }}" class="h-screen w-full border-0"></iframe>
@endsection

View File

@ -20,73 +20,48 @@
</div>
@endif
<div class="flex items-center justify-between">
<div class="flex items-center justify-between mt-4">
<section class="flex items-center">
<div class="items-center" style="display: none" id="pagination-button-container">
<button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300"
id="previous-page-button" title="Previous page">
<button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300" id="previous-page-button" title="Previous page">
<svg class="w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
</button>
<button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300"
id="next-page-button" title="Next page">
<button class="input-label focus:outline-none hover:text-blue-600 transition ease-in-out duration-300" id="next-page-button" title="Next page">
<svg class="w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</button>
</div>
<span class="text-sm text-gray-700 ml-2">{{ ctrans('texts.page') }}:
<span class="text-sm text-gray-700 ml-2 lg:hidden">{{ ctrans('texts.page') }}:
<span id="current-page-container"></span>
<span>{{ strtolower(ctrans('texts.of')) }}</span>
<span id="total-page-container"></span>
</span>
</section>
<section class="flex items-center space-x-1">
<div class="flex items-center mr-4 space-x-1">
<span class="text-gray-600 mr-2" id="zoom-level">175%</span>
<div class="flex items-center mr-4 space-x-1 lg:hidden">
<span class="text-gray-600 mr-2" id="zoom-level">100%</span>
<a href="#" id="zoom-in">
<svg class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600 cursor-pointer"
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="11" y1="8" x2="11" y2="14"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
<svg class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600 cursor-pointer" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
</a>
<a href="#" id="zoom-out">
<svg class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600 cursor-pointer"
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
<svg class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600 cursor-pointer" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
</a>
</div>
<div x-data="{ open: false }" @keydown.escape="open = false" @click.away="open = false"
class="relative inline-block text-left">
<div x-data="{ open: false }" @keydown.escape="open = false" @click.away="open = false" class="relative inline-block text-left">
<div>
<button @click="open = !open"
class="flex items-center text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600">
<button @click="open = !open" class="flex items-center text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<path
d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"/>
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
</svg>
</button>
</div>
<div x-show="open" x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
<div class="rounded-md bg-white shadow-xs">
<div class="py-1">
<a target="_blank" href="?mode=fullscreen"
class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ ctrans('texts.open_in_new_tab') }}</a>
<a target="_blank" href="?mode=fullscreen" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ ctrans('texts.open_in_new_tab') }}</a>
</div>
</div>
</div>
@ -94,9 +69,11 @@
</section>
</div>
<div class="flex justify-center">
<div class="flex justify-center lg:hidden">
<canvas id="pdf-placeholder" class="shadow rounded-lg bg-white mt-4 p-4"></canvas>
</div>
<iframe src="{{ $quote->pdf_file_path() }}" class="h-screen w-full border-0 mt-4"></iframe>
@endsection
@section('footer')

View File

@ -1,16 +0,0 @@
@extends('portal.ninja2020.layout.clean')
@section('meta_title', ctrans('texts.view_quote'))
@section('body')
@if(!$quote->isApproved())
@component('portal.ninja2020.quotes.includes.actions', ['quote' => $quote])
@section('quote-not-approved-right-side')
<a href="{{ route('client.quote.show', $quote->hashed_id) }}?mode=portal" class="mr-4 text-primary">
&#8592; {{ ctrans('texts.client_portal') }}
</a>
@endsection
@endcomponent
@endif
<iframe src="{{ $quote->pdf_file_path() }}" class="h-screen w-full border-0"></iframe>
@endsection

View File

@ -36,6 +36,8 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::put('clients/{client}/upload', 'ClientController@upload')->name('clients.upload');
Route::post('clients/bulk', 'ClientController@bulk')->name('clients.bulk');
Route::post('connected_account', 'ConnectedAccountController@index');
Route::resource('client_statement', 'ClientStatementController@statement'); // name = (client_statement. index / create / show / update / destroy / edit
Route::post('companies/purge/{company}', 'MigrationController@purgeCompany')->middleware('password_protected');
@ -146,6 +148,9 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::resource('tokens', 'TokenController')->middleware('password_protected'); // name = (tokens. index / create / show / update / destroy / edit
Route::post('tokens/bulk', 'TokenController@bulk')->name('tokens.bulk')->middleware('password_protected');
Route::get('settings/enable_two_factor', 'TwoFactorController@setupTwoFactor');
Route::post('settings/enable_two_factor', 'TwoFactorController@enableTwoFactor');
Route::resource('vendors', 'VendorController'); // name = (vendors. index / create / show / update / destroy / edit
Route::post('vendors/bulk', 'VendorController@bulk')->name('vendors.bulk');
Route::put('vendors/{vendor}/upload', 'VendorController@upload');
@ -156,6 +161,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::post('users/{user}/attach_to_company', 'UserController@attach')->middleware('password_protected');
Route::delete('users/{user}/detach_from_company', 'UserController@detach')->middleware('password_protected');
Route::post('users/bulk', 'UserController@bulk')->name('users.bulk')->middleware('password_protected');
Route::post('/user/{user}/reconfirm', 'UserController@reconfirm')->middleware('password_protected');
Route::resource('webhooks', 'WebhookController');
Route::post('webhooks/bulk', 'WebhookController@bulk')->name('webhooks.bulk');
@ -171,4 +177,6 @@ Route::match(['get', 'post'], 'payment_webhook/{company_key}/{company_gateway_id
->middleware(['guest', 'api_db'])
->name('payment_webhook');
Route::post('postmark_webhook', 'PostMarkController@webhook');
Route::fallback('BaseController@notFound');

View File

@ -74,7 +74,7 @@ class ImportCsvTest extends TestCase
$data = [
'hash' => $hash,
'column_map' => [ 'client' => $column_map ],
'column_map' => [ 'client' => [ 'mapping' => $column_map ] ],
'skip_header' => true,
'import_type' => 'csv',
];
@ -106,7 +106,7 @@ class ImportCsvTest extends TestCase
$data = [
'hash' => $hash,
'column_map' => [ 'client' => $column_map ],
'column_map' => [ 'client' => [ 'mapping' => $column_map ] ],
'skip_header' => true,
'import_type' => 'csv',
];
@ -139,7 +139,7 @@ class ImportCsvTest extends TestCase
$data = [
'hash' => $hash,
'column_map' => [ 'invoice' => $column_map ],
'column_map' => [ 'invoice' => [ 'mapping' => $column_map ] ],
'skip_header' => true,
'import_type' => 'csv',
];
@ -167,7 +167,7 @@ class ImportCsvTest extends TestCase
$data = [
'hash' => $hash,
'column_map' => [ 'vendor' => $column_map ],
'column_map' => [ 'vendor' => [ 'mapping' => $column_map ] ],
'skip_header' => true,
'import_type' => 'csv',
];
@ -192,7 +192,7 @@ class ImportCsvTest extends TestCase
$data = [
'hash' => $hash,
'column_map' => [ 'product' => $column_map ],
'column_map' => [ 'product' => [ 'mapping' => $column_map ] ],
'skip_header' => true,
'import_type' => 'csv',
];
@ -217,7 +217,7 @@ class ImportCsvTest extends TestCase
$data = [
'hash' => $hash,
'column_map' => [ 'expense' => $column_map ],
'column_map' => [ 'expense' => [ 'mapping' => $column_map ] ],
'skip_header' => true,
'import_type' => 'csv',
];
@ -249,7 +249,7 @@ class ImportCsvTest extends TestCase
$data = [
'hash' => $hash,
'column_map' => [ 'client' => $column_map ],
'column_map' => [ 'client' => [ 'mapping' => $column_map ] ],
'skip_header' => true,
'import_type' => 'csv',
];
@ -282,7 +282,7 @@ class ImportCsvTest extends TestCase
$data = [
'hash' => $hash,
'column_map' => [ 'invoice' => $column_map ],
'column_map' => [ 'invoice' => [ 'mapping' => $column_map ] ],
'skip_header' => true,
'import_type' => 'csv',
];
@ -308,7 +308,7 @@ class ImportCsvTest extends TestCase
$data = [
'hash' => $hash,
'column_map' => [ 'payment' => $column_map ],
'column_map' => [ 'payment' => [ 'mapping' => $column_map ] ],
'skip_header' => true,
'import_type' => 'csv',
];
@ -348,4 +348,4 @@ class ImportCsvTest extends TestCase
return $data;
}
}
}