mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-11 05:32:39 +01:00
commit
96e847be7d
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@ -48,6 +48,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo rm -rf bootstrap/cache/*
|
sudo rm -rf bootstrap/cache/*
|
||||||
sudo rm -rf node_modules
|
sudo rm -rf node_modules
|
||||||
|
- name: Prune Git History
|
||||||
|
run: |
|
||||||
|
sudo git gc
|
||||||
|
sudo git gc --aggressive
|
||||||
|
sudo git prune
|
||||||
- name: Build project # This would actually build your project, using zip for an example artifact
|
- name: Build project # This would actually build your project, using zip for an example artifact
|
||||||
run: |
|
run: |
|
||||||
zip -r ./invoiceninja.zip .* -x "../*"
|
zip -r ./invoiceninja.zip .* -x "../*"
|
||||||
|
@ -1 +1 @@
|
|||||||
5.1.10
|
5.1.11
|
@ -652,13 +652,12 @@ class CompanySettings extends BaseSettings
|
|||||||
'total_columns' => [
|
'total_columns' => [
|
||||||
'$subtotal',
|
'$subtotal',
|
||||||
'$discount',
|
'$discount',
|
||||||
'$total_taxes',
|
|
||||||
'$line_taxes',
|
|
||||||
'$custom_surcharge1',
|
'$custom_surcharge1',
|
||||||
'$custom_surcharge2',
|
'$custom_surcharge2',
|
||||||
'$custom_surcharge3',
|
'$custom_surcharge3',
|
||||||
'$custom_surcharge4',
|
'$custom_surcharge4',
|
||||||
'$total',
|
'$total_taxes',
|
||||||
|
'$line_taxes',
|
||||||
'$paid_to_date',
|
'$paid_to_date',
|
||||||
'$outstanding',
|
'$outstanding',
|
||||||
],
|
],
|
||||||
|
@ -13,6 +13,7 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Events\Credit\CreditWasEmailed;
|
use App\Events\Credit\CreditWasEmailed;
|
||||||
use App\Events\Quote\QuoteWasEmailed;
|
use App\Events\Quote\QuoteWasEmailed;
|
||||||
|
use App\Http\Middleware\UserVerified;
|
||||||
use App\Http\Requests\Email\SendEmailRequest;
|
use App\Http\Requests\Email\SendEmailRequest;
|
||||||
use App\Jobs\Entity\EmailEntity;
|
use App\Jobs\Entity\EmailEntity;
|
||||||
use App\Jobs\Mail\EntitySentMailer;
|
use App\Jobs\Mail\EntitySentMailer;
|
||||||
@ -130,7 +131,8 @@ class EmailController extends BaseController
|
|||||||
|
|
||||||
$entity_obj->service()->markSent()->save();
|
$entity_obj->service()->markSent()->save();
|
||||||
|
|
||||||
EmailEntity::dispatch($invitation, $invitation->company, $template, $data)->delay(now()->addSeconds(5));
|
EmailEntity::dispatch($invitation, $invitation->company, $template, $data)
|
||||||
|
->delay(now()->addSeconds(5));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,10 +78,8 @@ class PostMarkController extends BaseController
|
|||||||
|
|
||||||
$this->invitation = $this->discoverInvitation($request->input('MessageID'));
|
$this->invitation = $this->discoverInvitation($request->input('MessageID'));
|
||||||
|
|
||||||
if($this->invitation){
|
if($this->invitation)
|
||||||
$this->invitation->email_error = $request->input('Details');
|
$this->invitation->email_error = $request->input('Details');
|
||||||
$this->invitation->save();
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
return response()->json(['message' => 'Message not found']);
|
return response()->json(['message' => 'Message not found']);
|
||||||
|
|
||||||
@ -122,6 +120,9 @@ class PostMarkController extends BaseController
|
|||||||
// }
|
// }
|
||||||
private function processDelivery($request)
|
private function processDelivery($request)
|
||||||
{
|
{
|
||||||
|
$this->invitation->email_status = 'delivered';
|
||||||
|
$this->invitation->save();
|
||||||
|
|
||||||
SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_DELIVERY, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client);
|
SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_DELIVERY, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,6 +154,9 @@ class PostMarkController extends BaseController
|
|||||||
|
|
||||||
private function processBounce($request)
|
private function processBounce($request)
|
||||||
{
|
{
|
||||||
|
$this->invitation->email_status = 'bounced';
|
||||||
|
$this->invitation->save();
|
||||||
|
|
||||||
SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client);
|
SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,6 +187,10 @@ class PostMarkController extends BaseController
|
|||||||
// }
|
// }
|
||||||
private function processSpamComplaint($request)
|
private function processSpamComplaint($request)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
$this->invitation->email_status = 'spam';
|
||||||
|
$this->invitation->save();
|
||||||
|
|
||||||
SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client);
|
SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,6 +386,7 @@ class UserController extends BaseController
|
|||||||
/* When changing email address we store the former email in case we need to rollback */
|
/* When changing email address we store the former email in case we need to rollback */
|
||||||
if ($old_user_email != $new_email) {
|
if ($old_user_email != $new_email) {
|
||||||
$user->last_confirmed_email_address = $old_user_email;
|
$user->last_confirmed_email_address = $old_user_email;
|
||||||
|
$user->email_verified_at = null;
|
||||||
$user->save();
|
$user->save();
|
||||||
UserEmailChanged::dispatch($new_user, json_decode($old_user), auth()->user()->company());
|
UserEmailChanged::dispatch($new_user, json_decode($old_user), auth()->user()->company());
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ use App\Http\Middleware\TokenAuth;
|
|||||||
use App\Http\Middleware\TrimStrings;
|
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\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;
|
||||||
@ -157,5 +158,6 @@ class Kernel extends HttpKernel
|
|||||||
'phantom_secret' => PhantomSecret::class,
|
'phantom_secret' => PhantomSecret::class,
|
||||||
'contact_key_login' => ContactKeyLogin::class,
|
'contact_key_login' => ContactKeyLogin::class,
|
||||||
'check_client_existence' => CheckClientExistence::class,
|
'check_client_existence' => CheckClientExistence::class,
|
||||||
|
'user_verified' => UserVerified::class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,7 @@ class PasswordProtection
|
|||||||
|
|
||||||
/* Cannot allow duplicates! */
|
/* Cannot allow duplicates! */
|
||||||
if ($existing_user = MultiDB::hasUser($query)) {
|
if ($existing_user = MultiDB::hasUser($query)) {
|
||||||
|
Cache::add(auth()->user()->email.'_logged_in', Str::random(64), now()->addMinutes(30));
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
52
app/Http/Middleware/UserVerified.php
Normal file
52
app/Http/Middleware/UserVerified.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?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\Middleware;
|
||||||
|
|
||||||
|
use App\Libraries\MultiDB;
|
||||||
|
use App\Models\User;
|
||||||
|
use Closure;
|
||||||
|
use Hashids\Hashids;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class UserVerified.
|
||||||
|
*/
|
||||||
|
class UserVerified
|
||||||
|
{
|
||||||
|
public $user;
|
||||||
|
|
||||||
|
public function __construct(?User $user)
|
||||||
|
{
|
||||||
|
$this->user = $user ?: auth()->user();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param Closure $next
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
|
||||||
|
$error = [
|
||||||
|
'message' => 'Email confirmation required.',
|
||||||
|
'errors' => new \stdClass,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->user && !$this->user->isVerified())
|
||||||
|
return response()->json($error, 403);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
@ -39,9 +39,9 @@ class StoreUserRequest extends Request
|
|||||||
$rules['last_name'] = 'required|string|max:100';
|
$rules['last_name'] = 'required|string|max:100';
|
||||||
|
|
||||||
if (config('ninja.db.multi_db_enabled')) {
|
if (config('ninja.db.multi_db_enabled')) {
|
||||||
$rules['email'] = ['email', new ValidUserForCompany(), Rule::unique('users')];
|
$rules['email'] = ['email', new ValidUserForCompany(), Rule::unique('users')->ignore($this->input('company_user.account.id'), 'account_id')];
|
||||||
} else {
|
} else {
|
||||||
$rules['email'] = ['email',Rule::unique('users')];
|
$rules['email'] = ['email',Rule::unique('users')->ignore($this->input('company_user.account.id'), 'account_id')];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -56,6 +56,10 @@ class StoreUserRequest extends Request
|
|||||||
{
|
{
|
||||||
$input = $this->all();
|
$input = $this->all();
|
||||||
|
|
||||||
|
nlog($this->input('company_user.account'));
|
||||||
|
// nlog($this->input('company_user.account.id'));
|
||||||
|
// nlog($this->input('company_user.account.id'));
|
||||||
|
|
||||||
if (isset($input['company_user'])) {
|
if (isset($input['company_user'])) {
|
||||||
if (! isset($input['company_user']['is_admin'])) {
|
if (! isset($input['company_user']['is_admin'])) {
|
||||||
$input['company_user']['is_admin'] = false;
|
$input['company_user']['is_admin'] = false;
|
||||||
|
@ -26,7 +26,7 @@ class ValidUserForCompany implements Rule
|
|||||||
*/
|
*/
|
||||||
public function passes($attribute, $value)
|
public function passes($attribute, $value)
|
||||||
{
|
{
|
||||||
return MultiDB::checkUserAndCompanyCoExist($value, auth()->user()->company()->company_key);
|
return MultiDB::checkUserAndCompanyCoExist($value, auth()->user()->company()->company_key, auth()->user()->company()->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,8 +110,7 @@ class CreateAccount
|
|||||||
|
|
||||||
NinjaMailerJob::dispatch($nmo);
|
NinjaMailerJob::dispatch($nmo);
|
||||||
|
|
||||||
|
// NinjaMailerJob::dispatchNow($nmo);
|
||||||
NinjaMailerJob::dispatchNow($nmo);
|
|
||||||
|
|
||||||
VersionCheck::dispatchNow();
|
VersionCheck::dispatchNow();
|
||||||
|
|
||||||
|
@ -214,7 +214,7 @@ class Import implements ShouldQueue
|
|||||||
// if($check_data['status'] == 'errors')
|
// if($check_data['status'] == 'errors')
|
||||||
// throw new ProcessingMigrationArchiveFailed(implode("\n", $check_data));
|
// throw new ProcessingMigrationArchiveFailed(implode("\n", $check_data));
|
||||||
|
|
||||||
Mail::to($this->user)
|
Mail::to($this->user->email, $this->user->name())
|
||||||
->send(new MigrationCompleted($this->company, implode("<br>",$check_data)));
|
->send(new MigrationCompleted($this->company, implode("<br>",$check_data)));
|
||||||
|
|
||||||
/*After a migration first some basic jobs to ensure the system is up to date*/
|
/*After a migration first some basic jobs to ensure the system is up to date*/
|
||||||
|
@ -152,6 +152,11 @@ class Credit extends BaseModel
|
|||||||
return $this->hasMany(CreditInvitation::class);
|
return $this->hasMany(CreditInvitation::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function project()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Project::class)->withTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The invoice which the credit has been created from.
|
* The invoice which the credit has been created from.
|
||||||
*/
|
*/
|
||||||
|
@ -149,6 +149,11 @@ class Quote extends BaseModel
|
|||||||
return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed();
|
return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function project()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Project::class)->withTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
public function invitations()
|
public function invitations()
|
||||||
{
|
{
|
||||||
return $this->hasMany(QuoteInvitation::class);
|
return $this->hasMany(QuoteInvitation::class);
|
||||||
|
@ -167,6 +167,11 @@ class RecurringInvoice extends BaseModel
|
|||||||
return $this->belongsTo(Client::class)->withTrashed();
|
return $this->belongsTo(Client::class)->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function project()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Project::class)->withTrashed();
|
||||||
|
}
|
||||||
|
|
||||||
public function user()
|
public function user()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class)->withTrashed();
|
return $this->belongsTo(User::class)->withTrashed();
|
||||||
|
@ -80,6 +80,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
'custom_value3',
|
'custom_value3',
|
||||||
'custom_value4',
|
'custom_value4',
|
||||||
'is_deleted',
|
'is_deleted',
|
||||||
|
'google_2fa_secret',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -338,6 +339,11 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
return $this->morphMany(Document::class, 'documentable');
|
return $this->morphMany(Document::class, 'documentable');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isVerified()
|
||||||
|
{
|
||||||
|
return is_null($this->email_verified_at) ? false : true;
|
||||||
|
}
|
||||||
|
|
||||||
public function getEmailVerifiedAt()
|
public function getEmailVerifiedAt()
|
||||||
{
|
{
|
||||||
if ($this->email_verified_at) {
|
if ($this->email_verified_at) {
|
||||||
|
@ -63,7 +63,7 @@ class CreditCard
|
|||||||
'amount' => $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision),
|
'amount' => $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision),
|
||||||
'currency' => $this->stripe->client->getCurrencyCode(),
|
'currency' => $this->stripe->client->getCurrencyCode(),
|
||||||
'customer' => $this->stripe->findOrCreateCustomer(),
|
'customer' => $this->stripe->findOrCreateCustomer(),
|
||||||
'description' => collect($data['invoices'])->pluck('id'), // TODO: More meaningful description.
|
'description' => ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number'), // TODO: More meaningful description.
|
||||||
];
|
];
|
||||||
|
|
||||||
$payment_intent_data['setup_future_usage'] = 'off_session';
|
$payment_intent_data['setup_future_usage'] = 'off_session';
|
||||||
|
@ -407,7 +407,11 @@ class Design extends BaseDesign
|
|||||||
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'product_table-product.tax2-td']];
|
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'product_table-product.tax2-td']];
|
||||||
} elseif ($cell == '$product.tax_rate3') {
|
} elseif ($cell == '$product.tax_rate3') {
|
||||||
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'product_table-product.tax3-td']];
|
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => 'product_table-product.tax3-td']];
|
||||||
} else {
|
}
|
||||||
|
else if($cell == '$product.unit_cost' || $cell == '$task.rate') {
|
||||||
|
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['style' => 'white-space: nowrap;', 'data-ref' => "{$_type}_table-" . substr($cell, 1) . '-td']];
|
||||||
|
}
|
||||||
|
else {
|
||||||
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => "{$_type}_table-" . substr($cell, 1) . '-td']];
|
$element['elements'][] = ['element' => 'td', 'content' => $row[$cell], 'properties' => ['data-ref' => "{$_type}_table-" . substr($cell, 1) . '-td']];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,8 @@ class CreditInvitationTransformer extends EntityTransformer
|
|||||||
'updated_at' => (int) $invitation->updated_at,
|
'updated_at' => (int) $invitation->updated_at,
|
||||||
'archived_at' => (int) $invitation->deleted_at,
|
'archived_at' => (int) $invitation->deleted_at,
|
||||||
'created_at' => (int) $invitation->created_at,
|
'created_at' => (int) $invitation->created_at,
|
||||||
|
'email_status' => $invitation->email_status,
|
||||||
|
'email_error' => (string)$invitation->email_error,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,9 @@ class InvoiceInvitationTransformer extends EntityTransformer
|
|||||||
'opened_date' => $invitation->opened_date ?: '',
|
'opened_date' => $invitation->opened_date ?: '',
|
||||||
'updated_at' => (int) $invitation->updated_at,
|
'updated_at' => (int) $invitation->updated_at,
|
||||||
'archived_at' => (int) $invitation->deleted_at,
|
'archived_at' => (int) $invitation->deleted_at,
|
||||||
'created_at' => (int) $invitation->created_at,
|
'created_at' => (int) $invitation->created_at,
|
||||||
|
'email_status' => $invitation->email_status,
|
||||||
|
'email_error' => (string)$invitation->email_error,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,8 @@ class QuoteInvitationTransformer extends EntityTransformer
|
|||||||
'updated_at' => (int) $invitation->updated_at,
|
'updated_at' => (int) $invitation->updated_at,
|
||||||
'archived_at' => (int) $invitation->deleted_at,
|
'archived_at' => (int) $invitation->deleted_at,
|
||||||
'created_at' => (int) $invitation->created_at,
|
'created_at' => (int) $invitation->created_at,
|
||||||
|
'email_status' => $invitation->email_status,
|
||||||
|
'email_error' => (string)$invitation->email_error,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,8 @@ class RecurringInvoiceInvitationTransformer extends EntityTransformer
|
|||||||
'updated_at' => (int) $invitation->updated_at,
|
'updated_at' => (int) $invitation->updated_at,
|
||||||
'archived_at' => (int) $invitation->deleted_at,
|
'archived_at' => (int) $invitation->deleted_at,
|
||||||
'created_at' => (int) $invitation->created_at,
|
'created_at' => (int) $invitation->created_at,
|
||||||
|
'email_status' => $invitation->email_status,
|
||||||
|
'email_error' => (string)$invitation->email_error,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,7 @@ class UserTransformer extends EntityTransformer
|
|||||||
'custom_value4' => $user->custom_value4 ?: '',
|
'custom_value4' => $user->custom_value4 ?: '',
|
||||||
'oauth_provider_id' => (string) $user->oauth_provider_id,
|
'oauth_provider_id' => (string) $user->oauth_provider_id,
|
||||||
'last_confirmed_email_address' => (string) $user->last_confirmed_email_address ?: '',
|
'last_confirmed_email_address' => (string) $user->last_confirmed_email_address ?: '',
|
||||||
|
'google_2fa_secret' => (bool) $user->google_2fa_secret,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,7 +248,6 @@ class HtmlEngine
|
|||||||
$data['$client.currency'] = ['value' => $this->client->currency()->code, 'label' => ''];
|
$data['$client.currency'] = ['value' => $this->client->currency()->code, 'label' => ''];
|
||||||
|
|
||||||
$data['$client.balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')];
|
$data['$client.balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')];
|
||||||
$data['$outstanding'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')];
|
|
||||||
$data['$client_balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')];
|
$data['$client_balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')];
|
||||||
$data['$paid_to_date'] = ['value' => Number::formatMoney($this->client->paid_to_date, $this->client), 'label' => ctrans('texts.paid_to_date')];
|
$data['$paid_to_date'] = ['value' => Number::formatMoney($this->client->paid_to_date, $this->client), 'label' => ctrans('texts.paid_to_date')];
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ class TemplateEngine
|
|||||||
{
|
{
|
||||||
if (strlen($this->entity) > 1 && strlen($this->entity_id) > 1) {
|
if (strlen($this->entity) > 1 && strlen($this->entity_id) > 1) {
|
||||||
$class = 'App\Models\\'.ucfirst($this->entity);
|
$class = 'App\Models\\'.ucfirst($this->entity);
|
||||||
$this->entity_obj = $class::whereId($this->decodePrimaryKey($this->entity_id))->company()->first();
|
$this->entity_obj = $class::withTrashed()->where('id', $this->decodePrimaryKey($this->entity_id))->company()->first();
|
||||||
} else {
|
} else {
|
||||||
$this->mockEntity();
|
$this->mockEntity();
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@ use App\Models\Invoice;
|
|||||||
use App\Models\Quote;
|
use App\Models\Quote;
|
||||||
use App\Utils\Helpers;
|
use App\Utils\Helpers;
|
||||||
use App\Utils\Number;
|
use App\Utils\Number;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class MakesInvoiceValues.
|
* Class MakesInvoiceValues.
|
||||||
@ -289,8 +291,9 @@ trait MakesInvoiceValues
|
|||||||
$data[$key][$table_type.'.product_key'] = is_null(optional($item)->product_key) ? $item->item : $item->product_key;
|
$data[$key][$table_type.'.product_key'] = is_null(optional($item)->product_key) ? $item->item : $item->product_key;
|
||||||
$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'] = $item->notes;
|
|
||||||
$data[$key][$table_type.'.description'] = $item->notes;
|
$data[$key][$table_type.'.notes'] = $this->processReservedKeywords($item->notes);
|
||||||
|
$data[$key][$table_type.'.description'] = $this->processReservedKeywords($item->notes);
|
||||||
|
|
||||||
|
|
||||||
$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->client->company->custom_fields, "{$_table_type}1", $item->custom_value1, $this->client);
|
||||||
@ -298,7 +301,7 @@ trait MakesInvoiceValues
|
|||||||
$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->client->company->custom_fields, "{$_table_type}3", $item->custom_value3, $this->client);
|
||||||
$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->client->company->custom_fields, "{$_table_type}4", $item->custom_value4, $this->client);
|
||||||
|
|
||||||
$data[$key][$table_type.'.quantity'] = $item->quantity;
|
$data[$key][$table_type.'.quantity'] = Number::formatValue($item->quantity, $this->client->currency());
|
||||||
|
|
||||||
$data[$key][$table_type.'.unit_cost'] = Number::formatMoney($item->cost, $this->client);
|
$data[$key][$table_type.'.unit_cost'] = Number::formatMoney($item->cost, $this->client);
|
||||||
$data[$key][$table_type.'.cost'] = Number::formatMoney($item->cost, $this->client);
|
$data[$key][$table_type.'.cost'] = Number::formatMoney($item->cost, $this->client);
|
||||||
@ -351,6 +354,150 @@ trait MakesInvoiceValues
|
|||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process reserved words like :MONTH :YEAR :QUARTER
|
||||||
|
* as well as their operations.
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
private function processReservedKeywords(string $value): ?string
|
||||||
|
{
|
||||||
|
Carbon::setLocale($this->client->locale());
|
||||||
|
|
||||||
|
$replacements = [
|
||||||
|
'literal' => [
|
||||||
|
':MONTH' => now()->localeMonth,
|
||||||
|
':YEAR' => now()->year,
|
||||||
|
':QUARTER' => 'Q' . now()->quarter,
|
||||||
|
],
|
||||||
|
'raw' => [
|
||||||
|
':MONTH' => now()->month,
|
||||||
|
':YEAR' => now()->year,
|
||||||
|
':QUARTER' => now()->quarter,
|
||||||
|
],
|
||||||
|
'ranges' => [
|
||||||
|
'MONTHYEAR' => Carbon::createFromDate(now()->year, now()->month),
|
||||||
|
],
|
||||||
|
'ranges_raw' => [
|
||||||
|
'MONTH' => now()->month,
|
||||||
|
'YEAR' => now()->year,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// First case, with ranges.
|
||||||
|
preg_match_all('/\[(.*?)]/', $value, $ranges);
|
||||||
|
|
||||||
|
$matches = array_shift($ranges);
|
||||||
|
|
||||||
|
foreach ($matches as $match) {
|
||||||
|
if (!Str::contains($match, '|')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Str::contains($match, '|')) {
|
||||||
|
$parts = explode('|', $match); // [ '[MONTH', 'MONTH+2]' ]
|
||||||
|
|
||||||
|
$left = substr($parts[0], 1); // 'MONTH'
|
||||||
|
$right = substr($parts[1], 0, -1); // MONTH+2
|
||||||
|
|
||||||
|
// If left side is not part of replacements, skip.
|
||||||
|
if (!array_key_exists($left, $replacements['ranges'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$_left = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
|
||||||
|
$_right = '';
|
||||||
|
|
||||||
|
// If right side doesn't have any calculations, replace with raw ranges keyword.
|
||||||
|
if (!Str::contains($right, ['-', '+', '/', '*'])) {
|
||||||
|
$_right = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If right side contains one of math operations, calculate.
|
||||||
|
if (Str::contains($right, ['+'])) {
|
||||||
|
$operation = preg_match_all('/(?!^-)[+*\/-](\s?-)?/', $right, $_matches);
|
||||||
|
|
||||||
|
$_operation = array_shift($_matches)[0]; // + -
|
||||||
|
|
||||||
|
$_value = explode($_operation, $right); // [MONTHYEAR, 4]
|
||||||
|
|
||||||
|
$_right = Carbon::createFromDate(now()->year, now()->month)->addMonths($_value[1])->translatedFormat('F Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
$replacement = sprintf('%s to %s', $_left, $_right);
|
||||||
|
|
||||||
|
$value = preg_replace(
|
||||||
|
sprintf('/%s/', preg_quote($match)), $replacement, $value, 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Second case with more common calculations.
|
||||||
|
preg_match_all('/:([^:\s]+)/', $value, $common);
|
||||||
|
|
||||||
|
$matches = array_shift($common);
|
||||||
|
|
||||||
|
foreach ($matches as $match) {
|
||||||
|
$matches = collect($replacements['literal'])->filter(function ($value, $key) use ($match) {
|
||||||
|
return Str::startsWith($match, $key);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($matches->count() === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Str::contains($match, ['-', '+', '/', '*'])) {
|
||||||
|
$value = preg_replace(
|
||||||
|
sprintf('/%s/', $matches->keys()->first()), $replacements['literal'][$matches->keys()->first()], $value, 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Str::contains($match, ['-', '+', '/', '*'])) {
|
||||||
|
$operation = preg_match_all('/(?!^-)[+*\/-](\s?-)?/', $match, $_matches);
|
||||||
|
|
||||||
|
$_operation = array_shift($_matches)[0];
|
||||||
|
|
||||||
|
$_value = explode($_operation, $match); // [:MONTH, 4]
|
||||||
|
|
||||||
|
$raw = strtr($matches->keys()->first(), $replacements['raw']); // :MONTH => 1
|
||||||
|
|
||||||
|
$number = $res = preg_replace("/[^0-9]/", '', $_value[1]); // :MONTH+1. || :MONTH+2! => 1 || 2
|
||||||
|
|
||||||
|
$target = "/{$matches->keys()->first()}\\{$_operation}{$number}/"; // /:$KEYWORD\\$OPERATION$VALUE => /:MONTH\\+1
|
||||||
|
|
||||||
|
$output = (int) $raw + (int)$_value[1];
|
||||||
|
|
||||||
|
if ($operation == '+') {
|
||||||
|
$output = (int) $raw + (int)$_value[1]; // 1 (:MONTH) + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_operation == '-') {
|
||||||
|
$output = (int)$raw - (int)$_value[1]; // 1 (:MONTH) - 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_operation == '/') {
|
||||||
|
$output = (int)$raw / (int)$_value[1]; // 1 (:MONTH) / 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_operation == '*') {
|
||||||
|
$output = (int)$raw * (int)$_value[1]; // 1 (:MONTH) * 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($matches->keys()->first() == ':MONTH') {
|
||||||
|
$output = \Carbon\Carbon::create()->month($output)->localeMonth;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = preg_replace(
|
||||||
|
$target, $output, $value, 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Due to the way we are compiling the blade template we
|
* Due to the way we are compiling the blade template we
|
||||||
* have no ability to iterate, so in the case
|
* have no ability to iterate, so in the case
|
||||||
|
@ -13,7 +13,7 @@ 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', ''),
|
'app_domain' => env('APP_DOMAIN', ''),
|
||||||
'app_version' => '5.1.10',
|
'app_version' => '5.1.11',
|
||||||
'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', false),
|
'api_secret' => env('API_SECRET', false),
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class EnumInvitationsEmailStatus extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('invoice_invitations', function(Blueprint $table){
|
||||||
|
$table->enum('email_status', ['delivered', 'bounced', 'spam'])->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('quote_invitations', function(Blueprint $table){
|
||||||
|
$table->enum('email_status', ['delivered', 'bounced', 'spam'])->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('credit_invitations', function(Blueprint $table){
|
||||||
|
$table->enum('email_status', ['delivered', 'bounced', 'spam'])->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('recurring_invoice_invitations', function(Blueprint $table){
|
||||||
|
$table->enum('email_status', ['delivered', 'bounced', 'spam'])->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
2
public/flutter_service_worker.js
vendored
2
public/flutter_service_worker.js
vendored
@ -3,7 +3,7 @@ const MANIFEST = 'flutter-app-manifest';
|
|||||||
const TEMP = 'flutter-temp-cache';
|
const TEMP = 'flutter-temp-cache';
|
||||||
const CACHE_NAME = 'flutter-app-cache';
|
const CACHE_NAME = 'flutter-app-cache';
|
||||||
const RESOURCES = {
|
const RESOURCES = {
|
||||||
"main.dart.js": "3720742fd85b1fbea07bcf1bd87689c0",
|
"main.dart.js": "cf51da53acb137a76d1c8466fac1c0c1",
|
||||||
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
|
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
|
||||||
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
|
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
|
||||||
"favicon.ico": "51636d3a390451561744c42188ccd628",
|
"favicon.ico": "51636d3a390451561744c42188ccd628",
|
||||||
|
166316
public/main.dart.js
vendored
166316
public/main.dart.js
vendored
File diff suppressed because one or more lines are too long
@ -65,7 +65,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
|
|||||||
Route::get('documents/{document}/download', 'DocumentController@download')->name('documents.download');
|
Route::get('documents/{document}/download', 'DocumentController@download')->name('documents.download');
|
||||||
Route::post('documents/bulk', 'DocumentController@bulk')->name('documents.bulk');
|
Route::post('documents/bulk', 'DocumentController@bulk')->name('documents.bulk');
|
||||||
|
|
||||||
Route::post('emails', 'EmailController@send')->name('email.send');
|
Route::post('emails', 'EmailController@send')->name('email.send')->middleware('user_verified');
|
||||||
|
|
||||||
Route::resource('expenses', 'ExpenseController'); // name = (expenses. index / create / show / update / destroy / edit
|
Route::resource('expenses', 'ExpenseController'); // name = (expenses. index / create / show / update / destroy / edit
|
||||||
Route::put('expenses/{expense}/upload', 'ExpenseController@upload');
|
Route::put('expenses/{expense}/upload', 'ExpenseController@upload');
|
||||||
|
Loading…
Reference in New Issue
Block a user