mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-06-23 20:00:33 -04:00
Merge pull request #3791 from turbo124/v2
Fixes for payments affecting the client ledger balance
This commit is contained in:
commit
0dc8b59ecc
@ -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
|
||||
```
|
||||
|
@ -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;
|
||||
|
@ -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(),
|
||||
]);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ 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);
|
||||
}
|
||||
|
@ -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();
|
||||
// }
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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,
|
||||
];
|
||||
});
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 35 KiB |
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user