Integration works for Verifactu

This commit is contained in:
David Bomba 2025-08-13 13:15:51 +10:00
parent ff92756dbc
commit 3d3b5f6938
9 changed files with 46 additions and 37 deletions

View File

@ -51,6 +51,7 @@ class InvoiceBackupCast implements CastsAttributes
'document_type' => $value->document_type, 'document_type' => $value->document_type,
'child_invoice_ids' => $value->child_invoice_ids->toArray(), 'child_invoice_ids' => $value->child_invoice_ids->toArray(),
'redirect' => $value->redirect, 'redirect' => $value->redirect,
'adjustable_amount' => $value->adjustable_amount,
]) ])
]; ];
} }

View File

@ -27,9 +27,10 @@ class InvoiceBackup implements Castable
public Cancellation $cancellation = new Cancellation(0,0), public Cancellation $cancellation = new Cancellation(0,0),
public ?string $parent_invoice_id = null, // The id of the invoice that was cancelled public ?string $parent_invoice_id = null, // The id of the invoice that was cancelled
public ?string $parent_invoice_number = null, // The number of the invoice that was cancelled public ?string $parent_invoice_number = null, // The number of the invoice that was cancelled
public ?string $document_type = null, // The reason for the cancellation public ?string $document_type = null, // F1, R2
public Collection $child_invoice_ids = new Collection(), // Collection of child invoice IDs public Collection $child_invoice_ids = new Collection(), // Collection of child invoice IDs
public ?string $redirect = null, // The redirect url for the invoice public ?string $redirect = null, // The redirect url for the invoice
public float $adjustable_amount = 0,
) {} ) {}
/** /**
@ -51,7 +52,8 @@ class InvoiceBackup implements Castable
parent_invoice_number: $data['parent_invoice_number'] ?? null, parent_invoice_number: $data['parent_invoice_number'] ?? null,
document_type: $data['document_type'] ?? null, document_type: $data['document_type'] ?? null,
child_invoice_ids: isset($data['child_invoice_ids']) ? collect($data['child_invoice_ids']) : new Collection(), child_invoice_ids: isset($data['child_invoice_ids']) ? collect($data['child_invoice_ids']) : new Collection(),
redirect: $data['redirect'] ?? null redirect: $data['redirect'] ?? null,
adjustable_amount: $data['adjustable_amount'] ?? 0,
); );
} }

View File

@ -91,8 +91,7 @@ class StoreInvoiceRequest extends Request
$rules['custom_surcharge4'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999']; $rules['custom_surcharge4'] = ['sometimes', 'nullable', 'bail', 'numeric', 'max:99999999999999'];
$rules['location_id'] = ['nullable', 'sometimes','bail', Rule::exists('locations', 'id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)]; $rules['location_id'] = ['nullable', 'sometimes','bail', Rule::exists('locations', 'id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)];
$rules['verifactu_modified'] = ['bail', 'boolean', 'required_with:modified_invoice_id']; $rules['modified_invoice_id'] = ['bail', 'sometimes', 'nullable', new CanGenerateModificationInvoice()];
$rules['modified_invoice_id'] = ['bail', 'required_with:verifactu_modified', new CanGenerateModificationInvoice()];
return $rules; return $rules;
} }

View File

@ -53,8 +53,6 @@ class CanGenerateModificationInvoice implements ValidationRule
$fail("No se puede crear una factura de rectificación cuando se ha realizado un pago."); // Cannot create a rectification invoice where a payment has been made $fail("No se puede crear una factura de rectificación cuando se ha realizado un pago."); // Cannot create a rectification invoice where a payment has been made
} elseif($invoice->status_id === Invoice::STATUS_CANCELLED ) { } elseif($invoice->status_id === Invoice::STATUS_CANCELLED ) {
$fail("No se puede crear una factura de rectificación para una factura cancelada."); // Cannot create a rectification invoice for a cancelled invoice $fail("No se puede crear una factura de rectificación para una factura cancelada."); // Cannot create a rectification invoice for a cancelled invoice
} elseif($invoice->status_id === Invoice::STATUS_REPLACED) {
$fail("No se puede crear una factura de rectificación para una factura reemplazada."); // Cannot create a rectification invoice for a replaced invoice
} elseif($invoice->status_id === Invoice::STATUS_REVERSED) { } elseif($invoice->status_id === Invoice::STATUS_REVERSED) {
$fail("No se puede crear una factura de rectificación para una factura revertida."); // Cannot create a rectification invoice for a reversed invoice $fail("No se puede crear una factura de rectificación para una factura revertida."); // Cannot create a rectification invoice for a reversed invoice
} }

View File

@ -13,6 +13,7 @@
namespace App\Http\ValidationRules\Invoice; namespace App\Http\ValidationRules\Invoice;
use Closure; use Closure;
use App\Models\Client;
use App\Models\Invoice; use App\Models\Invoice;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
@ -40,6 +41,12 @@ class VerifactuAmountCheck implements ValidationRule
if ($company->verifactuEnabled()) { if ($company->verifactuEnabled()) {
$client = Client::withTrashed()->find($this->input['client_id']);
if($client->country->iso_3166_2 !== 'ES') {
return;
}
$invoice = false; $invoice = false;
$child_invoices = false; $child_invoices = false;
$child_invoice_totals = 0; $child_invoice_totals = 0;
@ -47,12 +54,18 @@ class VerifactuAmountCheck implements ValidationRule
if(isset($this->input['modified_invoice_id'])) { if(isset($this->input['modified_invoice_id'])) {
$invoice = Invoice::withTrashed()->where('id', $this->decodePrimaryKey($this->input['modified_invoice_id']))->company()->firstOrFail(); $invoice = Invoice::withTrashed()->where('id', $this->decodePrimaryKey($this->input['modified_invoice_id']))->company()->firstOrFail();
if ($invoice->backup->adjustable_amount <= 0) {
$fail("Invoice already credited in full");
}
$child_invoices = Invoice::withTrashed() $child_invoices = Invoice::withTrashed()
->whereIn('id', $this->transformKeys($invoice->backup->child_invoice_ids->toArray())) ->whereIn('id', $this->transformKeys($invoice->backup->child_invoice_ids->toArray()))
->get(); ->get();
$child_invoice_totals = round($child_invoices->sum('amount'), 2); $child_invoice_totals = round($child_invoices->sum('amount'), 2);
$child_invoice_count = $child_invoices->count(); $child_invoice_count = $child_invoices->count();
} }
$items = collect($this->input['line_items'])->map(function ($item) use($company){ $items = collect($this->input['line_items'])->map(function ($item) use($company){
@ -84,10 +97,7 @@ class VerifactuAmountCheck implements ValidationRule
$total = $items->sum() - $total_discount; $total = $items->sum() - $total_discount;
if($total > 0) { if($total < 0 && !$invoice) {
$fail("Only negative amounts allowed for rectification {$total}");
}
elseif($total < 0 && !$invoice) {
$fail("Negative invoices {$total} can only be linked to existing invoices"); $fail("Negative invoices {$total} can only be linked to existing invoices");
} }
elseif($invoice && ($total + $child_invoice_totals + $invoice->amount) < 0) { elseif($invoice && ($total + $child_invoice_totals + $invoice->amount) < 0) {

View File

@ -248,8 +248,6 @@ class Invoice extends BaseModel
public const STATUS_UNPAID = -2; // status < 4 || < 3 && !is_deleted && !trashed() public const STATUS_UNPAID = -2; // status < 4 || < 3 && !is_deleted && !trashed()
public const STATUS_REPLACED = 7; // handle the case where the invoice is replaced by another invoice.
public function toSearchableArray() public function toSearchableArray()
{ {
$locale = $this->company->locale(); $locale = $this->company->locale();

View File

@ -330,8 +330,8 @@ class BaseRepository
} }
/** Verifactu modified invoice check */ /** Verifactu modified invoice check */
if(isset($data['verifactu_modified']) && $data['verifactu_modified']) { if($model->company->verifactuEnabled() && $client->country->iso_3166_2 === 'ES') {
$model->service()->modifyVerifactuWorkflow($data['modified_invoice_id'])->save(); $model->service()->modifyVerifactuWorkflow($data, $this->new_model)->save();
} }
} }

View File

@ -710,34 +710,34 @@ class InvoiceService
} }
/** /**
* modifyVerifactuWorkflow * Handles all requirements for verifactu saves
* @todo - handle invoice modifications - ensure when we
* sent this to AEAT we reference the invoice that was replaced.
* *
* @param string $modified_invoice_hashed_id * @param array $invoice_array
* @param bool $new_model
* @return self * @return self
*/ */
public function modifyVerifactuWorkflow(string $modified_invoice_hashed_id): self public function modifyVerifactuWorkflow(array $invoice_array, bool $new_model): self
{ {
//if the new invoice has a negative amount - then it is not a replacement, it is a if($new_model && $this->invoice->amount >= 0) {
//delta modification on an existing invoice. $this->invoice->backup->document_type = 'F1';
$modified_invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($modified_invoice_hashed_id)); $this->invoice->backup->adjustable_amount = $this->invoice->amount;
$this->invoice->saveQuietly();
if($this->invoice->amount > 0) {
$modified_invoice->status_id = Invoice::STATUS_REPLACED;
} }
elseif(isset($invoice_array['modified_invoice_id'])) {
$modified_invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($invoice_array['modified_invoice_id']));
$modified_invoice->backup->child_invoice_ids->push($this->invoice->hashed_id); $modified_invoice->backup->child_invoice_ids->push($this->invoice->hashed_id);
$modified_invoice->backup->adjustable_amount += $this->invoice->amount;
$modified_invoice->save(); $modified_invoice->save();
$this->markSent(); $this->markSent();
//Update the client balance by the delta amount from the previous invoice to this one. //Update the client balance by the delta amount from the previous invoice to this one.
$this->invoice->backup->parent_invoice_id = $modified_invoice->hashed_id; $this->invoice->backup->parent_invoice_id = $modified_invoice->hashed_id;
$this->invoice->backup->document_type = 'F3'; $this->invoice->backup->document_type = 'R2';
$this->invoice->saveQuietly(); $this->invoice->saveQuietly();
$this->invoice->client->service()->updateBalance(round(($this->invoice->amount - $modified_invoice->amount), 2)); $this->invoice->client->service()->updateBalance(round(($this->invoice->amount - $modified_invoice->amount), 2));
$this->sendVerifactu(); $this->sendVerifactu();
}
return $this; return $this;
} }

View File

@ -5575,6 +5575,7 @@ $lang = array(
'create_company_error_unauthorized' => 'You are not authorized to create a company. Only the account owner can create a company.', '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', '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 or modified', 'delete_disabled_verifactu' => 'You cannot delete an invoice once it has been cancelled or modified',
'rectify' => 'Rectificar',
); );
return $lang; return $lang;