diff --git a/README.md b/README.md index f0776a9ae2bb..6e333aa72af8 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ npm i npm run production ``` -Please Note: Your APP_KEY in the .env file is used to encrypt data, if you loose this you will not be able to run the application. +Please Note: Your APP_KEY in the .env file is used to encrypt data, if you lose this you will not be able to run the application. Run if you want to load sample data, remember to configure .env ``` diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 3444d6983efe..5c3473f15eac 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -291,7 +291,7 @@ class BaseController extends Controller * Thresholds for displaying large account on first load */ if (request()->has('first_load') && request()->input('first_load') == 'true') { - if (auth()->user()->getCompany()->invoices->count() > 1000) { + if (auth()->user()->getCompany()->invoices->count() > 1000 || auth()->user()->getCompany()->products->count() > 1000) { $data = $mini_load; } else { $data = $first_load; diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index 798f780e2229..f34980d6f431 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -11,6 +11,7 @@ namespace App\Http\Controllers; +use App\DataMapper\CompanySettings; use App\DataMapper\DefaultSettings; use App\Http\Requests\Company\CreateCompanyRequest; use App\Http\Requests\Company\DestroyCompanyRequest; @@ -218,6 +219,7 @@ class CompanyController extends BaseController 'is_locked' => 0, 'permissions' => '', 'settings' => null, + 'notifications' => CompanySettings::notificationDefaults(), //'settings' => DefaultSettings::userSettings(), ]); diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 8649ccd1d749..e0e95cbc19f2 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -662,6 +662,13 @@ class InvoiceController extends BaseController case 'download': return response()->download(TempFile::path($invoice->pdf_file_path()), basename($invoice->pdf_file_path())); break; + case 'restore': + $this->invoice_repo->restore($invoice); + + if (!$bulk) { + return $this->listResponse($invoice); + } + break; case 'archive': $this->invoice_repo->archive($invoice); diff --git a/app/Http/Requests/Payment/UpdatePaymentRequest.php b/app/Http/Requests/Payment/UpdatePaymentRequest.php index 7ea172cf2c94..85539984270d 100644 --- a/app/Http/Requests/Payment/UpdatePaymentRequest.php +++ b/app/Http/Requests/Payment/UpdatePaymentRequest.php @@ -38,6 +38,7 @@ class UpdatePaymentRequest extends Request { return [ 'invoices' => ['required','array','min:1',new PaymentAppliedValidAmount,new ValidCreditsPresentRule], + 'invoices.*.invoice_id' => 'distinct', 'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', ]; } @@ -77,4 +78,11 @@ class UpdatePaymentRequest extends Request } $this->replace($input); } + + public function messages() + { + return [ + 'distinct' => 'Attemping duplicate payment on the same invoice Invoice', + ]; + } } diff --git a/app/Listeners/Invoice/UpdateInvoiceActivity.php b/app/Listeners/Invoice/UpdateInvoiceActivity.php index 9c7a0cb04015..5f7bbd8cad62 100644 --- a/app/Listeners/Invoice/UpdateInvoiceActivity.php +++ b/app/Listeners/Invoice/UpdateInvoiceActivity.php @@ -50,7 +50,8 @@ class UpdateInvoiceActivity implements ShouldQueue $fields->user_id = $event->invoice->user_id; $fields->company_id = $event->invoice->company_id; $fields->activity_type_id = Activity::UPDATE_INVOICE; - + $fields->invoice_id = $event->invoice->id; + $this->activity_repo->save($fields, $event->invoice); } } diff --git a/app/Models/Client.php b/app/Models/Client.php index 80846ac55ca5..636c2ebe64fb 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -251,12 +251,12 @@ class Client extends BaseModel implements HasLocalePreference * @param float $amount Adjustment amount * @return Client */ - public function processUnappliedPayment($amount) :Client - { - return $this->service()->updatePaidToDate($amount) - ->adjustCreditBalance($amount) - ->save(); - } + // public function processUnappliedPayment($amount) :Client + // { + // return $this->service()->updatePaidToDate($amount) + // ->adjustCreditBalance($amount) + // ->save(); + // } /** * diff --git a/app/Models/Presenters/ClientPresenter.php b/app/Models/Presenters/ClientPresenter.php index e3686629955b..b13d007437f0 100644 --- a/app/Models/Presenters/ClientPresenter.php +++ b/app/Models/Presenters/ClientPresenter.php @@ -25,15 +25,20 @@ class ClientPresenter extends EntityPresenter */ public function name() { + if($this->entity->name) + return $this->entity->name; + $contact = $this->entity->primary_contact->first(); $contact_name = 'No Contact Set'; - if ($contact) { + if ($contact && (strlen($contact->first_name) >=1 || strlen($contact->last_name) >=1)) { $contact_name = $contact->first_name. ' '. $contact->last_name; } + elseif($contact && (strlen($contact->email))) + $contact_name = $contact->email; - return $this->entity->name ?: $contact_name; + return $contact_name; } public function primary_contact_name() diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php index 6a1084bec687..277b15a6b984 100644 --- a/app/Repositories/BaseRepository.php +++ b/app/Repositories/BaseRepository.php @@ -263,7 +263,7 @@ class BaseRepository //make sure we are creating an invite for a contact who belongs to the client only! $contact = ClientContact::find($invitation['client_contact_id']); - if ($model->client_id == $contact->client_id); + if ($contact && $model->client_id == $contact->client_id); { $new_invitation = $invitation_factory_class::create($model->company_id, $model->user_id); $new_invitation->{$lcfirst_resource_id} = $model->id; diff --git a/app/Repositories/PaymentRepository.php b/app/Repositories/PaymentRepository.php index 76f893de179d..3f6e13c43410 100644 --- a/app/Repositories/PaymentRepository.php +++ b/app/Repositories/PaymentRepository.php @@ -46,7 +46,6 @@ class PaymentRepository extends BaseRepository /** * Saves and updates a payment. //todo refactor to handle refunds and payments. * - * * @param array $data the request object * @param Payment $payment The Payment object * @return Payment|null Payment $payment @@ -57,61 +56,85 @@ class PaymentRepository extends BaseRepository return $this->applyPayment($data, $payment); } - return $this->refundPayment($data, $payment); + return $payment; } /** * Handles a positive payment request - * @param array $data The data object - * @param Payment $payment The $payment entity + * @param array $data The data object + * @param Payment $payment The $payment entity * @return Payment The updated/created payment object */ private function applyPayment(array $data, Payment $payment): ?Payment { + //check currencies here and fill the exchange rate data if necessary if (!$payment->id) { $this->processExchangeRates($data, $payment); + + /*We only update the paid to date ONCE per payment*/ + if (array_key_exists('invoices', $data) && is_array($data['invoices']) && count($data['invoices']) > 0) { + + if($data['amount'] == '') + $data['amount'] = array_sum(array_column($data['invoices'], 'amount')); + + $client = Client::find($data['client_id']); + $client->service()->updatePaidToDate($data['amount'])->save(); + + } } + /*Fill the payment*/ $payment->fill($data); - $payment->status_id = Payment::STATUS_COMPLETED; - $payment->save(); + + /*Ensure payment number generated*/ if (!$payment->number || strlen($payment->number) == 0) { $payment->number = $payment->client->getNextPaymentNumber($payment->client); } - $payment->client->service()->updatePaidToDate($payment->amount)->save(); - $invoice_totals = 0; $credit_totals = 0; - if (array_key_exists('invoices', $data) && is_array($data['invoices'])) { + /*Iterate through invoices and apply payments*/ + if (array_key_exists('invoices', $data) && is_array($data['invoices']) && count($data['invoices']) > 0) { $invoice_totals = array_sum(array_column($data['invoices'], 'amount')); $invoices = Invoice::whereIn('id', array_column($data['invoices'], 'invoice_id'))->get(); $payment->invoices()->saveMany($invoices); + info("iterating through payment invoices"); + foreach ($data['invoices'] as $paid_invoice) { - $invoice = Invoice::whereId($paid_invoice['invoice_id'])->first(); + + $invoice = Invoice::whereId($paid_invoice['invoice_id'])->with('client')->first(); + + info("current client balance = {$invoice->client->balance}"); if ($invoice) { - $invoice->service()->applyPayment($payment, $paid_invoice['amount'])->save(); + + info("apply payment amount {$paid_invoice['amount']}"); + + $invoice = $invoice->service()->markSent()->applyPayment($payment, $paid_invoice['amount'])->save(); + + info("after processing invoice the client balance is now {$invoice->client->balance}"); + } + + } } else { - //payment is made, but not to any invoice, therefore we are applying the payment to the clients credit - $payment->client->processUnappliedPayment($payment->amount); + //payment is made, but not to any invoice, therefore we are applying the payment to the clients paid_to_date only + $payment->client->service()->updatePaidToDate($payment->amount)->save(); } if (array_key_exists('credits', $data) && is_array($data['credits'])) { $credit_totals = array_sum(array_column($data['credits'], 'amount')); $credits = Credit::whereIn('id', $this->transformKeys(array_column($data['credits'], 'credit_id')))->get(); - $payment->credits()->saveMany($credits); foreach ($data['credits'] as $paid_credit) { @@ -136,57 +159,9 @@ class PaymentRepository extends BaseRepository } $payment->save(); - return $payment->fresh(); } - /** - * @deprecated Refundable trait replaces this. - */ - private function refundPayment(array $data, Payment $payment): string - { - // //temp variable to sum the total refund/credit amount - // $invoice_total_adjustment = 0; - - // if (array_key_exists('invoices', $data) && is_array($data['invoices'])) { - - // foreach ($data['invoices'] as $adjusted_invoice) { - - // $invoice = Invoice::whereId($adjusted_invoice['invoice_id'])->first(); - - // $invoice_total_adjustment += $adjusted_invoice['amount']; - - // if (array_key_exists('credits', $adjusted_invoice)) { - - // //process and insert credit notes - // foreach ($adjusted_invoice['credits'] as $credit) { - - // $credit = $this->credit_repo->save($credit, CreditFactory::create(auth()->user()->id, auth()->user()->id), $invoice); - - // } - - // } else { - // //todo - generate Credit Note for $amount on $invoice - the assumption here is that it is a FULL refund - // } - - // } - - // if (array_key_exists('amount', $data) && $data['amount'] != $invoice_total_adjustment) - // return 'Amount must equal the sum of invoice adjustments'; - // } - - - // //adjust applied amount - // $payment->applied += $invoice_total_adjustment; - - // //adjust clients paid to date - // $client = $payment->client; - // $client->paid_to_date += $invoice_total_adjustment; - - // $payment->save(); - // $client->save(); - } - /** * If the client is paying in a currency other than diff --git a/app/Services/Invoice/ApplyPayment.php b/app/Services/Invoice/ApplyPayment.php index dda5d1fcce6e..d8f5ead9213c 100644 --- a/app/Services/Invoice/ApplyPayment.php +++ b/app/Services/Invoice/ApplyPayment.php @@ -37,7 +37,14 @@ class ApplyPayment extends AbstractService ->ledger() ->updatePaymentBalance($this->payment_amount*-1); - $this->payment->client->service()->updateBalance($this->payment_amount*-1)->save(); + info("apply paymenet method - current client balance = {$this->payment->client->balance}"); + + info("reducing client balance by payment amount {$this->payment_amount}"); + + $this->invoice->client->service()->updateBalance($this->payment_amount*-1)->save(); +// $this->invoice->client->service()->updateBalance($this->payment_amount*-1)->updatePaidToDate($this->payment_amount)->save(); + + info("post client balance = {$this->invoice->client->balance}"); /* Update Pivot Record amount */ $this->payment->invoices->each(function ($inv) { @@ -47,6 +54,10 @@ class ApplyPayment extends AbstractService } }); + $this->invoice->fresh('client'); + + info("1 end of apply payment method the client balnace = {$this->invoice->client->balance}"); + if ($this->invoice->hasPartial()) { //is partial and amount is exactly the partial amount if ($this->invoice->partial == $this->payment_amount) { @@ -61,9 +72,11 @@ class ApplyPayment extends AbstractService } elseif ($this->payment_amount < $this->invoice->balance) { //partial invoice payment made $this->invoice->service()->clearPartial()->setStatus(Invoice::STATUS_PARTIAL)->updateBalance($this->payment_amount*-1); } + info("2 end of apply payment method the client balnace = {$this->invoice->client->balance}"); $this->invoice->service()->applyNumber()->save(); + info("3 end of apply payment method the client balnace = {$this->invoice->client->balance}"); return $this->invoice; } } diff --git a/app/Services/Invoice/MarkSent.php b/app/Services/Invoice/MarkSent.php index f31175598b08..314777d35769 100644 --- a/app/Services/Invoice/MarkSent.php +++ b/app/Services/Invoice/MarkSent.php @@ -48,10 +48,14 @@ class MarkSent extends AbstractService ->setDueDate() ->save(); + info("marking invoice sent currently client balance = {$this->client->balance}"); + $this->client->service()->updateBalance($this->invoice->balance)->save(); + info("after marking invoice sent currently client balance = {$this->client->balance}"); + $this->invoice->ledger()->updateInvoiceBalance($this->invoice->balance); - return $this->invoice; + return $this->invoice->fresh(); } } diff --git a/app/Utils/Traits/Notifications/UserNotifies.php b/app/Utils/Traits/Notifications/UserNotifies.php index a7d4addf2caa..d5326910b249 100644 --- a/app/Utils/Traits/Notifications/UserNotifies.php +++ b/app/Utils/Traits/Notifications/UserNotifies.php @@ -50,6 +50,9 @@ trait UserNotifies $notifiable_methods = []; $notifications = $company_user->notifications; + if(!$notifications) + return []; + if ($entity->user_id == $company_user->_user_id || $entity->assigned_user_id == $company_user->user_id) { array_push($required_permissions, "all_user_notifications"); } diff --git a/database/factories/AccountFactory.php b/database/factories/AccountFactory.php index 67868546c159..dc068b1ae9ac 100644 --- a/database/factories/AccountFactory.php +++ b/database/factories/AccountFactory.php @@ -7,5 +7,6 @@ $factory->define(App\Models\Account::class, function (Faker $faker) { return [ 'default_company_id' => 1, 'key' => Str::random(32), + 'report_errors' => 1, ]; }); diff --git a/public/images/invoiceninja-black-logo-vertical.png b/public/images/invoiceninja-black-logo-vertical.png deleted file mode 100644 index 9ea0baffa92b..000000000000 Binary files a/public/images/invoiceninja-black-logo-vertical.png and /dev/null differ diff --git a/public/images/invoiceninja-black-logo.png b/public/images/invoiceninja-black-logo.png deleted file mode 100644 index b5458b71024f..000000000000 Binary files a/public/images/invoiceninja-black-logo.png and /dev/null differ diff --git a/public/images/logo.png b/public/images/logo.png index e88127bbbc8b..5141cd4d1e38 100644 Binary files a/public/images/logo.png and b/public/images/logo.png differ diff --git a/tests/Integration/CompanyLedgerTest.php b/tests/Integration/CompanyLedgerTest.php index cac91786f1a6..905c2b77f246 100644 --- a/tests/Integration/CompanyLedgerTest.php +++ b/tests/Integration/CompanyLedgerTest.php @@ -171,6 +171,7 @@ class CompanyLedgerTest extends TestCase $invoice = Invoice::find($this->decodePrimaryKey($acc['data']['id'])); + //client->balance should = 10 $invoice->service()->markSent()->save(); $this->assertEquals($invoice->client->balance, 10); @@ -193,6 +194,7 @@ class CompanyLedgerTest extends TestCase $invoice = Invoice::find($this->decodePrimaryKey($acc['data']['id'])); $invoice->service()->markSent()->save(); + //client balance should = 20 $this->assertEquals($invoice->client->balance, 20); $invoice_ledger = $invoice->company_ledger->sortByDesc('id')->first(); @@ -211,7 +213,6 @@ class CompanyLedgerTest extends TestCase ], ], 'date' => '2020/12/11', - ]; $response = $this->withHeaders([ @@ -224,7 +225,8 @@ class CompanyLedgerTest extends TestCase $payment = Payment::find($this->decodePrimaryKey($acc['data']['id'])); $payment_ledger = $payment->company_ledger->sortByDesc('id')->first(); - $invoice->fresh(); + +info($payment->client->balance); $this->assertEquals($payment->client->balance, $payment_ledger->balance); $this->assertEquals($payment->client->paid_to_date, 10);