Merge pull request #6451 from turbo124/v5-develop

Fixes for carbon
This commit is contained in:
David Bomba 2021-08-13 18:35:50 +10:00 committed by GitHub
commit 756039fcc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 342 additions and 80 deletions

View File

@ -16,6 +16,7 @@ use App\Http\Requests\Setup\CheckDatabaseRequest;
use App\Http\Requests\Setup\CheckMailRequest; use App\Http\Requests\Setup\CheckMailRequest;
use App\Http\Requests\Setup\StoreSetupRequest; use App\Http\Requests\Setup\StoreSetupRequest;
use App\Jobs\Account\CreateAccount; use App\Jobs\Account\CreateAccount;
use App\Jobs\Util\SchedulerCheck;
use App\Jobs\Util\VersionCheck; use App\Jobs\Util\VersionCheck;
use App\Models\Account; use App\Models\Account;
use App\Utils\CurlUtils; use App\Utils\CurlUtils;
@ -279,10 +280,7 @@ class SetupController extends Controller
public function update() public function update()
{ {
// if(Ninja::isHosted())
// return redirect('/');
// if( Ninja::isNinja() || !request()->has('secret') || (request()->input('secret') != config('ninja.update_secret')) )
if(!request()->has('secret') || (request()->input('secret') != config('ninja.update_secret')) ) if(!request()->has('secret') || (request()->input('secret') != config('ninja.update_secret')) )
return redirect('/'); return redirect('/');
@ -311,6 +309,8 @@ class SetupController extends Controller
$this->buildCache(true); $this->buildCache(true);
SchedulerCheck::dispatchNow();
return redirect('/'); return redirect('/');
} }

View File

@ -134,6 +134,9 @@ class Account extends BaseModel
public function getPlan() public function getPlan()
{ {
if(Carbon::parse($this->plan_expires)->lt(now()))
return '';
return $this->plan ?: ''; return $this->plan ?: '';
} }

View File

@ -0,0 +1,128 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\Invoice;
use App\Events\Invoice\InvoiceWasPaid;
use App\Events\Payment\PaymentWasCreated;
use App\Factory\PaymentFactory;
use App\Jobs\Invoice\InvoiceWorkflowSettings;
use App\Jobs\Payment\EmailPayment;
use App\Libraries\Currency\Conversion\CurrencyApi;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\AbstractService;
use App\Services\Client\ClientService;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use Illuminate\Support\Carbon;
class ApplyPaymentAmount extends AbstractService
{
use GeneratesCounter;
private $invoice;
private $amount;
public function __construct(Invoice $invoice, $amount)
{
$this->invoice = $invoice;
$this->amount = $amount;
}
public function run()
{
if ($this->invoice->status_id == Invoice::STATUS_DRAFT) {
$this->invoice->service()->markSent();
}
/*Don't double pay*/
if ($this->invoice->statud_id == Invoice::STATUS_PAID) {
return $this->invoice;
}
if($this->amount == 0)
return $this->invoice;
/* Create Payment */
$payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id);
$payment->amount = $this->amount;
$payment->applied = min($this->amount, $this->invoice->balance);
$payment->number = $this->getNextPaymentNumber($this->invoice->client);
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->client_id = $this->invoice->client_id;
$payment->transaction_reference = ctrans('texts.manual_entry');
$payment->currency_id = $this->invoice->client->getSetting('currency_id');
$payment->is_manual = true;
/* Create a payment relationship to the invoice entity */
$payment->save();
$this->setExchangeRate($payment);
$payment->invoices()->attach($this->invoice->id, [
'amount' => $payment->amount,
]);
$this->invoice->next_send_date = null;
$this->invoice->service()
->setExchangeRate()
->updateBalance($payment->amount * -1)
->updatePaidToDate($payment->amount)
->setCalculatedStatus()
->applyNumber()
->deletePdf()
->save();
if ($this->invoice->client->getSetting('client_manual_payment_notification'))
$payment->service()->sendEmail();
/* Update Invoice balance */
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
event(new InvoiceWasPaid($this->invoice, $payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
$payment->ledger()
->updatePaymentBalance($payment->amount * -1);
$this->invoice
->client
->service()
->updateBalance($payment->amount * -1)
->updatePaidToDate($payment->amount)
->save();
$this->invoice->service()->workFlow()->save();
return $this->invoice;
}
private function setExchangeRate(Payment $payment)
{
$client_currency = $payment->client->getSetting('currency_id');
$company_currency = $payment->client->company->settings->currency_id;
if ($company_currency != $client_currency) {
$exchange_rate = new CurrencyApi();
$payment->exchange_rate = $exchange_rate->exchangeRate($client_currency, $company_currency, Carbon::parse($payment->date));
//$payment->exchange_currency_id = $client_currency; // 23/06/2021
$payment->exchange_currency_id = $company_currency;
$payment->save();
}
}
}

View File

@ -22,6 +22,7 @@ use App\Models\Payment;
use App\Models\Task; use App\Models\Task;
use App\Repositories\BaseRepository; use App\Repositories\BaseRepository;
use App\Services\Client\ClientService; use App\Services\Client\ClientService;
use App\Services\Invoice\ApplyPaymentAmount;
use App\Services\Invoice\UpdateReminder; use App\Services\Invoice\UpdateReminder;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
@ -51,6 +52,13 @@ class InvoiceService
return $this; return $this;
} }
public function applyPaymentAmount($amount)
{
$this->invoice = (new ApplyPaymentAmount($this->invoice, $amount))->run();
return $this;
}
/** /**
* Applies the invoice number. * Applies the invoice number.
* @return $this InvoiceService object * @return $this InvoiceService object

View File

@ -44,6 +44,10 @@ class TriggeredActions extends AbstractService
$this->invoice = $this->invoice->service()->markPaid()->save(); $this->invoice = $this->invoice->service()->markPaid()->save();
} }
if ($this->request->has('amount_paid') && is_numeric($this->request->input('amount_paid')) ) {
$this->invoice = $this->invoice->service()->applyPaymentAmount($this->request->input('amount_paid'))->save();
}
if ($this->request->has('send_email') && $this->request->input('send_email') == 'true') { if ($this->request->has('send_email') && $this->request->input('send_email') == 'true') {
$this->sendEmail(); $this->sendEmail();
} }
@ -52,6 +56,7 @@ class TriggeredActions extends AbstractService
$this->invoice = $this->invoice->service()->markSent()->save(); $this->invoice = $this->invoice->service()->markSent()->save();
} }
return $this->invoice; return $this->invoice;
} }

View File

@ -168,7 +168,12 @@ class HtmlEngine
$data['$invoice.discount'] = ['value' => Number::formatMoney($this->entity_calc->getTotalDiscount(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.discount')]; $data['$invoice.discount'] = ['value' => Number::formatMoney($this->entity_calc->getTotalDiscount(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.discount')];
$data['$discount'] = &$data['$invoice.discount']; $data['$discount'] = &$data['$invoice.discount'];
$data['$subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.subtotal')]; $data['$subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.subtotal')];
$data['$net_subtotal'] = ['value' => Number::formatMoney(($this->entity_calc->getSubTotal() - $this->entity->total_taxes), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.net_subtotal')];
if($this->entity->uses_inclusive_taxes)
$data['$net_subtotal'] = ['value' => Number::formatMoney(($this->entity_calc->getSubTotal() - $this->entity->total_taxes), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.net_subtotal')];
else
$data['$net_subtotal'] = ['value' => Number::formatMoney($this->entity_calc->getSubTotal(), $this->client) ?: '&nbsp;', 'label' => ctrans('texts.net_subtotal')];
$data['$invoice.subtotal'] = &$data['$subtotal']; $data['$invoice.subtotal'] = &$data['$subtotal'];
if ($this->entity->partial > 0) { if ($this->entity->partial > 0) {

View File

@ -1,27 +1,26 @@
<!DOCTYPE html> <!DOCTYPE html>
<html data-report-errors="{{ $report_errors }}" data-rc="{{ $rc }}"> <html data-report-errors="{{ $report_errors }}" data-rc="{{ $rc }}">
<head> <head>
<!-- Source: https://github.com/invoiceninja/invoiceninja --> <!-- Source: https://github.com/invoiceninja/invoiceninja -->
<!-- Version: {{ config('ninja.app_version') }} --> <!-- Version: {{ config('ninja.app_version') }} -->
<base href="/">
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Invoice Ninja</title>
<meta name="google-signin-client_id" content="{{ config('services.google.client_id') }}"> <meta name="google-signin-client_id" content="{{ config('services.google.client_id') }}">
<link rel="manifest" href="manifest.json?v={{ config('ninja.app_version') }}"> <link rel="manifest" href="manifest.json?v={{ config('ninja.app_version') }}">
<script src="{{ asset('js/pdf.min.js') }}"></script> <script src="{{ asset('js/pdf.min.js') }}"></script>
<script type="text/javascript"> <script type="text/javascript">
pdfjsLib.GlobalWorkerOptions.workerSrc = "{{ asset('js/pdf.worker.min.js') }}"; pdfjsLib.GlobalWorkerOptions.workerSrc = "{{ asset('js/pdf.worker.min.js') }}";
</script> </script>
</head> <meta content="IE=Edge" http-equiv="X-UA-Compatible">
<body style="background-color:#888888;"> <meta name="description" content="Invoice Clients, Track Work-Time, Get Paid Online.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="invoiceninja_client">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<title>Invoice Ninja</title>
<link rel="manifest" href="manifest.json">
<style> <style>
/* fix for blurry fonts
flt-glass-pane {
image-rendering: pixelated;
}
*/
/* https://projects.lukehaas.me/css-loaders/ */ /* https://projects.lukehaas.me/css-loaders/ */
.loader, .loader,
.loader:before, .loader:before,
@ -35,7 +34,7 @@
animation: load7 1.8s infinite ease-in-out; animation: load7 1.8s infinite ease-in-out;
} }
.loader { .loader {
color: #ffffff; color: #FFFFFF;
font-size: 10px; font-size: 10px;
margin: 80px auto; margin: 80px auto;
position: relative; position: relative;
@ -80,85 +79,85 @@
box-shadow: 0 2.5em 0 0; box-shadow: 0 2.5em 0 0;
} }
} }
</style> </style>
</head>
<body style="background-color:#888888;">
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script> <script>
@if (request()->clear_local) @if (request()->clear_local)
window.onload = function() { window.onload = function() {
window.localStorage.clear(); window.localStorage.clear();
} }
@endif @endif
var serviceWorkerVersion = null;
if ('serviceWorker' in navigator) { var scriptLoaded = false;
window.addEventListener('load', function () { function loadMainDartJs() {
navigator.serviceWorker.register('flutter_service_worker.js?v={{ config('ninja.app_version') }}'); if (scriptLoaded) {
}); return;
}
document.addEventListener('DOMContentLoaded', function(event) {
document.getElementById('loader').style.display = 'none';
});
function invokeServiceWorkerUpdateFlow() {
// you have a better UI here, reloading is not a great user experince here.
const confirmed = alert('New version of the app is available. Refresh now');
if (confirmed == true) {
window.location.reload();
} }
scriptLoaded = true;
var scriptTag = document.createElement('script');
@if(config('ninja.flutter_renderer') == 'hosted')
scriptTag.src = 'main.dart.js';
@else
scriptTag.src = 'main.foss.dart.js';
@endif
scriptTag.type = 'application/javascript';
document.body.append(scriptTag);
} }
async function handleServiceWorker() { if ('serviceWorker' in navigator) {
if ('serviceWorker' in navigator) { // Service workers are supported. Use them.
// get the ServiceWorkerRegistration instance window.addEventListener('load', function () {
const registration = await navigator.serviceWorker.getRegistration(); // Wait for registration to finish before dropping the <script> tag.
// (it is also returned from navigator.serviceWorker.register() function) // Otherwise, the browser will load the script multiple times,
// potentially different versions.
if (registration) { var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
// detect Service Worker update available and wait for it to become installed navigator.serviceWorker.register(serviceWorkerUrl)
registration.addEventListener('updatefound', () => { .then((reg) => {
if (registration.installing) { function waitForActivation(serviceWorker) {
// wait until the new Service worker is actually installed (ready to take over) serviceWorker.addEventListener('statechange', () => {
registration.installing.addEventListener('statechange', () => { if (serviceWorker.state == 'activated') {
if (registration.waiting) { console.log('Installed new service worker.');
// if there's an existing controller (previous Service Worker), show the prompt loadMainDartJs();
if (navigator.serviceWorker.controller) {
invokeServiceWorkerUpdateFlow(registration);
} else {
// otherwise it's the first install, nothing to do
console.log('Service Worker initialized for the first time');
}
} }
}); });
} }
}); if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
let refreshing = false; // one for the first time. Simply wait for it to activate.
waitForActivation(reg.installing ?? reg.waiting);
// detect controller change and refresh the page } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
navigator.serviceWorker.addEventListener('controllerchange', () => { // When the app updates the serviceWorkerVersion changes, so we
if (!refreshing) { // need to ask the service worker to update.
window.location.reload(); console.log('New service worker available.');
refreshing = true; reg.update();
waitForActivation(reg.installing);
} else {
// Existing service worker is still good.
console.log('Loading app from service worker.');
loadMainDartJs();
} }
}); });
} // If service worker doesn't succeed in a reasonable amount of time,
} // fallback to plaint <script> tag.
setTimeout(() => {
if (!scriptLoaded) {
console.warn(
'Failed to load app from service worker. Falling back to plain <script> tag.',
);
loadMainDartJs();
}
}, 4000);
});
} else {
// Service workers not supported. Just drop the <script> tag.
loadMainDartJs();
} }
handleServiceWorker();
</script> </script>
@if(config('ninja.flutter_renderer') == 'hosted')
<script defer src="main.dart.js?v={{ config('ninja.app_version') }}" type="application/javascript"></script>
@else
<script defer src="main.foss.dart.js?v={{ config('ninja.app_version') }}" type="application/javascript"></script>
@endif
<center style="padding-top: 150px" id="loader"> <center style="padding-top: 150px" id="loader">
<div class="loader"></div> <div class="loader"></div>
</center> </center>
</body> </body>
</html> </html>

View File

@ -0,0 +1,114 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace Tests\Feature;
use App\Factory\InvoiceItemFactory;
use App\Models\Client;
use App\Models\Invoice;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
*/
class InvoiceAmountPaymentTest extends TestCase
{
use DatabaseTransactions;
use MockAccountData;
public function setUp() :void
{
parent::setUp();
$this->makeTestData();
$this->withoutMiddleware(
ThrottleRequests::class
);
}
public function testPaymentAmountForInvoice()
{
$data = [
'name' => 'A Nice Client',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients', $data);
$response->assertStatus(200);
$arr = $response->json();
$client_hash_id = $arr['data']['id'];
$client = Client::find($this->decodePrimaryKey($client_hash_id));
$this->assertEquals($client->balance, 0);
$this->assertEquals($client->paid_to_date, 0);
//create new invoice.
$line_items = [];
$item = InvoiceItemFactory::create();
$item->quantity = 1;
$item->cost = 10;
$line_items[] = (array)$item;
$item = InvoiceItemFactory::create();
$item->quantity = 1;
$item->cost = 10;
$line_items[] = (array)$item;
$invoice = [
'status_id' => 1,
'number' => '',
'discount' => 0,
'is_amount_discount' => 1,
'po_number' => '3434343',
'public_notes' => 'notes',
'is_deleted' => 0,
'custom_value1' => 0,
'custom_value2' => 0,
'custom_value3' => 0,
'custom_value4' => 0,
'client_id' => $client_hash_id,
'line_items' => (array)$line_items,
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/invoices?amount_paid=10', $invoice)
->assertStatus(200);
$arr = $response->json();
$invoice_one_hashed_id = $arr['data']['id'];
$invoice = Invoice::find($this->decodePrimaryKey($invoice_one_hashed_id));
$this->assertEquals(10, $invoice->balance);
$this->assertTrue($invoice->payments()->exists());
$payment = $invoice->payments()->first();
$this->assertEquals(10, $payment->applied);
$this->assertEquals(10, $payment->amount);
}
}