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,
'child_invoice_ids' => $value->child_invoice_ids->toArray(),
'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 ?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 $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 ?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,
document_type: $data['document_type'] ?? null,
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['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', 'required_with:verifactu_modified', new CanGenerateModificationInvoice()];
$rules['modified_invoice_id'] = ['bail', 'sometimes', 'nullable', new CanGenerateModificationInvoice()];
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
} 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
} 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) {
$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;
use Closure;
use App\Models\Client;
use App\Models\Invoice;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Validation\ValidationRule;
@ -39,6 +40,12 @@ class VerifactuAmountCheck implements ValidationRule
$company = $user->company();
if ($company->verifactuEnabled()) {
$client = Client::withTrashed()->find($this->input['client_id']);
if($client->country->iso_3166_2 !== 'ES') {
return;
}
$invoice = false;
$child_invoices = false;
@ -47,12 +54,18 @@ class VerifactuAmountCheck implements ValidationRule
if(isset($this->input['modified_invoice_id'])) {
$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()
->whereIn('id', $this->transformKeys($invoice->backup->child_invoice_ids->toArray()))
->get();
$child_invoice_totals = round($child_invoices->sum('amount'), 2);
$child_invoice_count = $child_invoices->count();
}
$items = collect($this->input['line_items'])->map(function ($item) use($company){
@ -84,10 +97,7 @@ class VerifactuAmountCheck implements ValidationRule
$total = $items->sum() - $total_discount;
if($total > 0) {
$fail("Only negative amounts allowed for rectification {$total}");
}
elseif($total < 0 && !$invoice) {
if($total < 0 && !$invoice) {
$fail("Negative invoices {$total} can only be linked to existing invoices");
}
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_REPLACED = 7; // handle the case where the invoice is replaced by another invoice.
public function toSearchableArray()
{
$locale = $this->company->locale();

View File

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

View File

@ -710,35 +710,35 @@ class InvoiceService
}
/**
* modifyVerifactuWorkflow
* @todo - handle invoice modifications - ensure when we
* sent this to AEAT we reference the invoice that was replaced.
* Handles all requirements for verifactu saves
*
* @param string $modified_invoice_hashed_id
* @param array $invoice_array
* @param bool $new_model
* @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
//delta modification on an existing invoice.
$modified_invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($modified_invoice_hashed_id));
if($this->invoice->amount > 0) {
$modified_invoice->status_id = Invoice::STATUS_REPLACED;
if($new_model && $this->invoice->amount >= 0) {
$this->invoice->backup->document_type = 'F1';
$this->invoice->backup->adjustable_amount = $this->invoice->amount;
$this->invoice->saveQuietly();
}
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->adjustable_amount += $this->invoice->amount;
$modified_invoice->save();
$this->markSent();
//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->document_type = 'R2';
$this->invoice->saveQuietly();
$this->invoice->client->service()->updateBalance(round(($this->invoice->amount - $modified_invoice->amount), 2));
$this->sendVerifactu();
}
$modified_invoice->backup->child_invoice_ids->push($this->invoice->hashed_id);
$modified_invoice->save();
$this->markSent();
//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->document_type = 'F3';
$this->invoice->saveQuietly();
$this->invoice->client->service()->updateBalance(round(($this->invoice->amount - $modified_invoice->amount), 2));
$this->sendVerifactu();
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.',
'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',
'rectify' => 'Rectificar',
);
return $lang;