mirror of
				https://github.com/invoiceninja/invoiceninja.git
				synced 2025-10-31 15:57: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,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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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