Commit 758eaebf authored by DevPilot's avatar DevPilot

feat(rentals): collect a rental invoice into a treasury from the invoice screen

The rent could be invoiced but not collected. `payInvoice` required a
`payment_id` that already existed, and the form asked the accountant to type
"رقم الدفعة من النظام" — a number they had no way to obtain, because nothing on
this screen created a payment. So a generated invoice could never be settled.

`collectInvoice` takes the money properly: pick the treasury, the method and the
date, and it writes the payment against that treasury, then settles the invoice
through the same RentalInvoiceService::markPaid the old path used — so the late
fee is still calculated from the due date and the journal entry is unchanged.

payInvoice is left in place for the case it was built for: a cashier who already
raised the receipt and has its number.

Verified end to end on a production clone: contract approved, 12 monthly
invoices generated on the contract's own payment_due_day, one collected into
«الخزنة الرئيسية», and the entry posted Dr الصندوق / Cr إيجار محلات + ض.ق.م with
the trial balance still at 0.00.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
parent 72d9ec28
...@@ -481,7 +481,10 @@ class RentalController extends Controller ...@@ -481,7 +481,10 @@ class RentalController extends Controller
$contract = RentalContract::find($contractId); $contract = RentalContract::find($contractId);
$entity = $contract ? RentalEntity::find((int) $contract->entity_id) : null; $entity = $contract ? RentalEntity::find((int) $contract->entity_id) : null;
$treasuries = App::getInstance()->db()->select("SELECT id, name_ar FROM treasuries WHERE is_active = 1 ORDER BY id");
return $this->view('Rentals.Views.invoice_show', [ return $this->view('Rentals.Views.invoice_show', [
'treasuries' => $treasuries,
'invoice' => $invoice, 'invoice' => $invoice,
'contract' => $contract, 'contract' => $contract,
'entity' => $entity, 'entity' => $entity,
...@@ -491,6 +494,89 @@ class RentalController extends Controller ...@@ -491,6 +494,89 @@ class RentalController extends Controller
/** /**
* Mark invoice as paid. * Mark invoice as paid.
*/ */
/**
* Collect a rental invoice — take the money and record it in one step.
*
* `payInvoice` below wants a payment id that already exists, which is fine
* when the cashier raised the receipt first, but left no way to collect the
* rent from this screen at all: the accountant had to go and manufacture a
* payment elsewhere and come back with its number. This creates the payment
* against the chosen treasury, then marks the invoice paid through the same
* service, so the late fee and the journal entry behave identically.
*/
public function collectInvoice(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$invoice = \App\Modules\Rentals\Models\RentalInvoice::find((int) $id);
if (!$invoice) {
return $this->redirect('/rentals')->withError('الفاتورة غير موجودة');
}
if (($invoice->status ?? '') === 'paid') {
return $this->redirect('/rentals/invoices/' . $id)->withError('الفاتورة مدفوعة بالفعل');
}
$treasuryId = (int) $request->post('treasury_id', 0);
if ($treasuryId <= 0) {
return $this->redirect('/rentals/invoices/' . $id)->withError('اختار الخزنة اللي الفلوس هتدخلها');
}
if (!$db->selectOne("SELECT id FROM treasuries WHERE id = ? AND is_active = 1", [$treasuryId])) {
return $this->redirect('/rentals/invoices/' . $id)->withError('الخزنة غير موجودة أو موقوفة');
}
$method = (string) $request->post('payment_method', 'cash');
if (!\in_array($method, ['cash', 'check', 'visa', 'transfer'], true)) {
$method = 'cash';
}
$paidAt = trim((string) $request->post('paid_at', '')) ?: date('Y-m-d H:i:s');
$paidAt = str_replace('T', ' ', $paidAt);
if (strlen($paidAt) === 16) {
$paidAt .= ':00';
}
// The invoice total is the amount due; the late fee is added by markPaid
// once it knows the payment date, so the receipt is written for what the
// service settles on rather than a number guessed here.
$db->beginTransaction();
try {
$paymentId = (int) $db->insert('payments', [
'payment_type' => 'rental_invoice',
'amount' => (string) ($invoice->total_amount ?? '0.00'),
'currency' => 'EGP',
'payment_method' => $method,
'check_number' => $method === 'check' ? ($request->post('reference') ?: null) : null,
'check_bank' => $method === 'check' ? ($request->post('bank') ?: null) : null,
'visa_reference' => $method === 'visa' ? ($request->post('reference') ?: null) : null,
'transfer_reference' => $method === 'transfer' ? ($request->post('reference') ?: null) : null,
'transfer_bank' => $method === 'transfer' ? ($request->post('bank') ?: null) : null,
'related_entity_type' => 'rental_invoice',
'related_entity_id' => (int) $id,
'payment_date' => substr($paidAt, 0, 10),
'received_by_employee_id' => $employee ? (int) $employee->id : null,
'treasury_id' => $treasuryId,
'notes' => 'تحصيل فاتورة إيجار رقم ' . ($invoice->invoice_number ?? $id),
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
return $this->redirect('/rentals/invoices/' . $id)->withError('تعذّر تسجيل الدفعة: ' . $e->getMessage());
}
try {
RentalInvoiceService::markPaid((int) $id, $paymentId, $paidAt);
} catch (\RuntimeException $e) {
return $this->redirect('/rentals/invoices/' . $id)->withError($e->getMessage());
}
return $this->redirect('/rentals/invoices/' . $id)
->withSuccess('اتحصّلت الفاتورة ودخلت الخزنة، واتعمل قيد الإيراد. رقم الدفعة: ' . $paymentId);
}
public function payInvoice(Request $request, string $id): Response public function payInvoice(Request $request, string $id): Response
{ {
$paymentId = (int) $request->post('payment_id', 0); $paymentId = (int) $request->post('payment_id', 0);
......
...@@ -21,5 +21,6 @@ return [ ...@@ -21,5 +21,6 @@ return [
['POST', '/rentals/contracts/{id:\d+}/invoices', 'Rentals\Controllers\RentalController@storeInvoice', ['auth', 'csrf'], 'rental.manage_contract'], ['POST', '/rentals/contracts/{id:\d+}/invoices', 'Rentals\Controllers\RentalController@storeInvoice', ['auth', 'csrf'], 'rental.manage_contract'],
['POST', '/rentals/contracts/{id:\d+}/invoices/bulk-generate', 'Rentals\Controllers\RentalController@bulkGenerateInvoices', ['auth', 'csrf'], 'rental.manage_contract'], ['POST', '/rentals/contracts/{id:\d+}/invoices/bulk-generate', 'Rentals\Controllers\RentalController@bulkGenerateInvoices', ['auth', 'csrf'], 'rental.manage_contract'],
['GET', '/rentals/invoices/{id:\d+}', 'Rentals\Controllers\RentalController@showInvoice', ['auth'], 'rental.view'], ['GET', '/rentals/invoices/{id:\d+}', 'Rentals\Controllers\RentalController@showInvoice', ['auth'], 'rental.view'],
['POST', '/rentals/invoices/{id:\d+}/collect', 'Rentals\Controllers\RentalController@collectInvoice', ['auth', 'csrf'], 'rental.manage_contract'],
['POST', '/rentals/invoices/{id:\d+}/pay', 'Rentals\Controllers\RentalController@payInvoice', ['auth', 'csrf'], 'rental.manage_contract'], ['POST', '/rentals/invoices/{id:\d+}/pay', 'Rentals\Controllers\RentalController@payInvoice', ['auth', 'csrf'], 'rental.manage_contract'],
]; ];
...@@ -107,17 +107,35 @@ $iColor = RentalInvoice::getStatusColor($iStatus); ...@@ -107,17 +107,35 @@ $iColor = RentalInvoice::getStatusColor($iStatus);
<?php if ($iStatus === 'unpaid' && can('rental.manage_contract')): ?> <?php if ($iStatus === 'unpaid' && can('rental.manage_contract')): ?>
<div class="card" style="margin-bottom:20px;padding:20px;background:#F0FDF4;border:1px solid #BBF7D0;"> <div class="card" style="margin-bottom:20px;padding:20px;background:#F0FDF4;border:1px solid #BBF7D0;">
<h4 style="margin:0 0 15px;color:#059669;"><i data-lucide="banknote" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> تسجيل الدفع</h4> <h4 style="margin:0 0 15px;color:#059669;"><i data-lucide="banknote" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> تسجيل الدفع</h4>
<form method="POST" action="/rentals/invoices/<?= (int) $invoice->id ?>/pay" style="display:grid;grid-template-columns:1fr 1fr auto;gap:12px;align-items:end;"> <form method="POST" action="/rentals/invoices/<?= (int) $invoice->id ?>/collect" style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr auto;gap:12px;align-items:end;">
<?= \App\Core\CSRF::field() ?> <?= \App\Core\CSRF::field() ?>
<div> <div>
<label class="form-label">رقم الدفعة <span style="color:#DC2626;">*</span></label> <label class="form-label">الخزنة <span style="color:#DC2626;">*</span></label>
<input type="number" name="payment_id" class="form-input" required placeholder="رقم الدفعة من النظام"> <select name="treasury_id" class="form-input" required>
<option value="">— اختار الخزنة —</option>
<?php foreach (($treasuries ?? []) as $t): ?>
<option value="<?= (int) $t['id'] ?>"><?= e((string) $t['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">طريقة الدفع</label>
<select name="payment_method" class="form-input" onchange="document.getElementById('refBox1').style.display = this.value === 'cash' ? 'none' : '';">
<option value="cash">نقدًا</option>
<option value="check">شيك</option>
<option value="visa">فيزا</option>
<option value="transfer">تحويل بنكي</option>
</select>
</div>
<div id="refBox1" style="display:none;">
<label class="form-label">رقم الشيك / المرجع</label>
<input type="text" name="reference" class="form-input" style="direction:ltr;text-align:left;">
</div> </div>
<div> <div>
<label class="form-label">تاريخ الدفع</label> <label class="form-label">تاريخ الدفع</label>
<input type="datetime-local" name="paid_at" class="form-input" value="<?= date('Y-m-d\TH:i') ?>"> <input type="datetime-local" name="paid_at" class="form-input" value="<?= date('Y-m-d\TH:i') ?>">
</div> </div>
<button type="submit" class="btn btn-primary">تسجيل الدفع</button> <button type="submit" class="btn btn-primary">تحصيل ودخول الخزنة</button>
</form> </form>
<?php if ($contract && ($contract->late_fee_type ?? 'none') !== 'none'): ?> <?php if ($contract && ($contract->late_fee_type ?? 'none') !== 'none'): ?>
<div style="margin-top:10px;font-size:12px;color:#6B7280;"> <div style="margin-top:10px;font-size:12px;color:#6B7280;">
...@@ -133,17 +151,35 @@ $iColor = RentalInvoice::getStatusColor($iStatus); ...@@ -133,17 +151,35 @@ $iColor = RentalInvoice::getStatusColor($iStatus);
<?php if ($iStatus === 'overdue' && can('rental.manage_contract')): ?> <?php if ($iStatus === 'overdue' && can('rental.manage_contract')): ?>
<div class="card" style="margin-bottom:20px;padding:20px;background:#FEF2F2;border:1px solid #FECACA;"> <div class="card" style="margin-bottom:20px;padding:20px;background:#FEF2F2;border:1px solid #FECACA;">
<h4 style="margin:0 0 15px;color:#DC2626;"><i data-lucide="alert-triangle" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> فاتورة متأخرة — تسجيل الدفع</h4> <h4 style="margin:0 0 15px;color:#DC2626;"><i data-lucide="alert-triangle" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> فاتورة متأخرة — تسجيل الدفع</h4>
<form method="POST" action="/rentals/invoices/<?= (int) $invoice->id ?>/pay" style="display:grid;grid-template-columns:1fr 1fr auto;gap:12px;align-items:end;"> <form method="POST" action="/rentals/invoices/<?= (int) $invoice->id ?>/collect" style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr auto;gap:12px;align-items:end;">
<?= \App\Core\CSRF::field() ?> <?= \App\Core\CSRF::field() ?>
<div> <div>
<label class="form-label">رقم الدفعة <span style="color:#DC2626;">*</span></label> <label class="form-label">الخزنة <span style="color:#DC2626;">*</span></label>
<input type="number" name="payment_id" class="form-input" required placeholder="رقم الدفعة"> <select name="treasury_id" class="form-input" required>
<option value="">— اختار الخزنة —</option>
<?php foreach (($treasuries ?? []) as $t): ?>
<option value="<?= (int) $t['id'] ?>"><?= e((string) $t['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">طريقة الدفع</label>
<select name="payment_method" class="form-input" onchange="document.getElementById('refBox2').style.display = this.value === 'cash' ? 'none' : '';">
<option value="cash">نقدًا</option>
<option value="check">شيك</option>
<option value="visa">فيزا</option>
<option value="transfer">تحويل بنكي</option>
</select>
</div>
<div id="refBox2" style="display:none;">
<label class="form-label">رقم الشيك / المرجع</label>
<input type="text" name="reference" class="form-input" style="direction:ltr;text-align:left;">
</div> </div>
<div> <div>
<label class="form-label">تاريخ الدفع</label> <label class="form-label">تاريخ الدفع</label>
<input type="datetime-local" name="paid_at" class="form-input" value="<?= date('Y-m-d\TH:i') ?>"> <input type="datetime-local" name="paid_at" class="form-input" value="<?= date('Y-m-d\TH:i') ?>">
</div> </div>
<button type="submit" class="btn btn-primary" style="background:#DC2626;">تسجيل الدفع + الغرامة</button> <button type="submit" class="btn btn-primary" style="background:#DC2626;">تحصيل ودخول الخزنة</button>
</form> </form>
</div> </div>
<?php endif; ?> <?php endif; ?>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment