diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index f6113d66a6..ab7b26e2db 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -785,7 +785,7 @@ class InvoiceController extends BaseController } break; case 'cancel': - $invoice = $invoice->service()->handleCancellation()->save(); + $invoice = $invoice->service()->handleCancellation(request()->input('reason'))->save(); if (! $bulk) { $this->itemResponse($invoice); } diff --git a/app/Http/ValidationRules/Invoice/RestoreDisabledRule.php b/app/Http/ValidationRules/Invoice/RestoreDisabledRule.php index 6913967e4f..c7bd214081 100644 --- a/app/Http/ValidationRules/Invoice/RestoreDisabledRule.php +++ b/app/Http/ValidationRules/Invoice/RestoreDisabledRule.php @@ -27,7 +27,7 @@ class RestoreDisabledRule implements ValidationRule public function validate(string $attribute, mixed $value, Closure $fail): void { - if (empty($value) || $value != 'restore') { + if (empty($value) ||!in_array($value, ['delete', 'restore'])) { return; } @@ -36,9 +36,13 @@ class RestoreDisabledRule implements ValidationRule $company = $user->company(); /** For verifactu, we do not allow restores of deleted invoices */ - if($company->verifactuEnabled() && Invoice::withTrashed()->whereIn('id', $this->transformKeys(request()->ids))->where('company_id', $company->id)->where('is_deleted', true)->exists()) { + if($company->verifactuEnabled() && $value == 'restore' &&Invoice::withTrashed()->whereIn('id', $this->transformKeys(request()->ids))->where('company_id', $company->id)->where('is_deleted', true)->exists()) { $fail(ctrans('texts.restore_disabled_verifactu')); } + + if ($company->verifactuEnabled() && $value == 'delete' && Invoice::withTrashed()->whereIn('id', $this->transformKeys(request()->ids))->where('company_id', $company->id)->where('status_id', Invoice::STATUS_CANCELLED)->exists()) { + $fail(ctrans('texts.delete_disabled_verifactu')); + } } } diff --git a/app/Services/Invoice/HandleCancellation.php b/app/Services/Invoice/HandleCancellation.php index 78d2196c2f..480f80cfef 100644 --- a/app/Services/Invoice/HandleCancellation.php +++ b/app/Services/Invoice/HandleCancellation.php @@ -13,6 +13,7 @@ namespace App\Services\Invoice; use App\Events\Invoice\InvoiceWasCancelled; use App\Models\Invoice; +use App\Repositories\InvoiceRepository; use App\Services\AbstractService; use App\Utils\Ninja; use App\Utils\Traits\GeneratesCounter; @@ -33,6 +34,10 @@ class HandleCancellation extends AbstractService return $this->invoice; } + if($this->invoice->company->verifactuEnabled()) { + return $this->verifactuCancellation(); + } + $adjustment = ($this->invoice->balance < 0) ? abs($this->invoice->balance) : $this->invoice->balance * -1; $this->backupCancellation($adjustment); @@ -55,6 +60,59 @@ class HandleCancellation extends AbstractService return $this->invoice; } + + private function verifactuCancellation(): Invoice + { + + $replicated_invoice = $this->invoice->replicate(); + + $this->invoice = $this->invoice->service()->setStatus(Invoice::STATUS_CANCELLED)->save(); + $this->invoice->service()->workFlow()->save(); + + $replicated_invoice->status_id = Invoice::STATUS_DRAFT; + $replicated_invoice->date = now()->format('Y-m-d'); + $replicated_invoice->due_date = null; + $replicated_invoice->partial = 0; + $replicated_invoice->partial_due_date = null; + $replicated_invoice->number = null; + $replicated_invoice->amount = 0; + $replicated_invoice->balance = 0; + $replicated_invoice->paid_to_date = 0; + + $items = $replicated_invoice->line_items; + + foreach($items as &$item) { + $item->quantity = $item->quantity * -1; + } + + $replicated_invoice->line_items = $items; + + $backup = new \stdClass(); + $backup->cancelled_invoice_id = $this->invoice->hashed_id; + $backup->cancelled_invoice_number = $this->invoice->number; + $backup->cancellation_reason = $this->reason ?? 'R3'; + + $replicated_invoice->backup = $backup; + + $invoice_repository = new InvoiceRepository(); + $replicated_invoice = $invoice_repository->save([], $replicated_invoice); + $replicated_invoice->service()->markSent()->sendVerifactu()->save(); + + $old_backup = new \stdClass(); + $old_backup->credit_invoice_id = $replicated_invoice->hashed_id; + $old_backup->credit_invoice_number = $replicated_invoice->number; + $old_backup->cancellation_reason = $this->reason ?? 'R3'; + + $this->invoice->backup = $old_backup; + $this->invoice->saveQuietly(); + $this->invoice->fresh(); + + event(new InvoiceWasCancelled($this->invoice, $this->invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); + event('eloquent.updated: App\Models\Invoice', $this->invoice); + + return $this->invoice; + } + public function reverse() { /* The stored cancelled object - contains the adjustment and status*/ diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index a75af86a91..714fdea6bd 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -674,6 +674,15 @@ class InvoiceService } + //@todo - verifactu + public function sendVerifactu() + { + // if($this->invoice->company->verifactuEnabled()) { + // (new SendVerifactu($this->invoice))->handle(); + // } + + return $this; + } /** * Saves the invoice. * @return Invoice object diff --git a/app/Utils/Traits/Invoice/ActionsInvoice.php b/app/Utils/Traits/Invoice/ActionsInvoice.php index cb5ab41388..37820d0f20 100644 --- a/app/Utils/Traits/Invoice/ActionsInvoice.php +++ b/app/Utils/Traits/Invoice/ActionsInvoice.php @@ -17,6 +17,11 @@ trait ActionsInvoice { public function invoiceDeletable($invoice): bool { + //Cancelled invoices are not deletable if verifactu is enabled + if($invoice->company->verifactuEnabled() && $invoice->status_id == Invoice::STATUS_CANCELLED) { + return false; + } + if ($invoice->status_id <= Invoice::STATUS_SENT && $invoice->is_deleted == false && $invoice->deleted_at == null && diff --git a/lang/en/texts.php b/lang/en/texts.php index dd3d41349d..ed1a8d977d 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5574,6 +5574,7 @@ $lang = array( 'selected_products' => 'Selected Products', 'create_company_error_unauthorized' => 'You are not authorized to create a company. Only the account owner can create a company.', 'restore_disabled_verifactu' => 'You cannot restore an invoice once it has been deleted', + 'delete_disabled_verifactu' => 'You cannot delete an invoice once it has been cancelled', ); return $lang; diff --git a/tests/Feature/EInvoice/Verifactu/VerifactuApiTest.php b/tests/Feature/EInvoice/Verifactu/VerifactuApiTest.php index 636628c727..2c3cf0f25a 100644 --- a/tests/Feature/EInvoice/Verifactu/VerifactuApiTest.php +++ b/tests/Feature/EInvoice/Verifactu/VerifactuApiTest.php @@ -11,6 +11,7 @@ namespace Tests\Feature\EInvoice\Verifactu; +use App\DataMapper\InvoiceItem; use Tests\TestCase; use App\Models\Client; use App\Models\Invoice; @@ -45,6 +46,92 @@ class VerifactuApiTest extends TestCase $this->makeTestData(); } + public function test_cancel_invoice_response() + { + + $item = new InvoiceItem(); + $item->quantity = 1; + $item->product_key = 'product_1'; + $item->notes = 'Product 1'; + $item->cost = 100; + $item->discount = 0; + $item->tax_rate1 = 21; + $item->tax_name1 = 'IVA'; + + $invoice = Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'user_id' => $this->user->id, + 'number' => 'INV-0001', + 'date' => now()->format('Y-m-d'), + 'due_date' => now()->addDays(100)->format('Y-m-d'), + 'status_id' => Invoice::STATUS_DRAFT, + 'is_deleted' => false, + 'tax_rate1' => 0, + 'tax_name1' => '', + 'tax_rate2' => 0, + 'tax_name2' => '', + 'tax_rate3' => 0, + 'tax_name3' => '', + 'line_items' => [$item], + 'discount' => 0, + 'uses_inclusive_taxes' => false, + 'exchange_rate' => 1, + 'partial' => 0, + 'partial_due_date' => null, + 'footer' => '', + ]); + + $repo = new InvoiceRepository(); + $invoice = $repo->save([], $invoice); + + $invoice->service()->markSent()->save(); + + $this->assertEquals($invoice->status_id, Invoice::STATUS_SENT); + $this->assertEquals($invoice->balance, 121); + $this->assertEquals($invoice->amount, 121); + + $settings = $this->company->settings; + $settings->e_invoice_type = 'verifactu'; + + $this->company->settings = $settings; + $this->company->save(); + + + $data = [ + 'action' => 'cancel', + 'ids' => [$invoice->hashed_id], + 'reason' => 'R3' + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/invoices/bulk', $data); + + $response->assertStatus(200); + + $arr = $response->json(); + + $this->assertEquals($arr['data'][0]['status_id'], Invoice::STATUS_CANCELLED); + $this->assertEquals($arr['data'][0]['balance'], 121); + $this->assertEquals($arr['data'][0]['amount'], 121); + $this->assertNotNull($arr['data'][0]['backup']['credit_invoice_id']); + $this->assertNotNull($arr['data'][0]['backup']['credit_invoice_number']); + $this->assertEquals($arr['data'][0]['backup']['cancellation_reason'], 'R3'); + + $credit_invoice = Invoice::find($this->decodePrimaryKey($arr['data'][0]['backup']['credit_invoice_id'])); + + nlog($credit_invoice->toArray()); + $this->assertNotNull($credit_invoice); + $this->assertEquals($credit_invoice->status_id, Invoice::STATUS_SENT); + $this->assertEquals($credit_invoice->balance, -121); + $this->assertEquals($credit_invoice->amount, -121); + $this->assertEquals($credit_invoice->backup->cancelled_invoice_id, $invoice->hashed_id); + $this->assertEquals($credit_invoice->backup->cancelled_invoice_number, $invoice->number); + $this->assertEquals($credit_invoice->backup->cancellation_reason, 'R3'); + } + public function test_restore_invoice_validation() {