Commit a3fd26ba authored by Mahmoud Aglan's avatar Mahmoud Aglan

accounting bugs

parent 6d44324b
......@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\BankAccount;
......@@ -98,11 +99,11 @@ class BankAccountController extends Controller
return $this->redirect('/accounting/bank-accounts');
}
public function edit(int $id): Response
public function edit(Request $request, string $id): Response
{
$this->authorize('accounting.bank_account.manage');
$account = BankAccount::findOrFail($id);
$account = BankAccount::findOrFail((int) $id);
$glAccounts = Account::query()
->where('is_archived', '=', 0)
......@@ -117,11 +118,11 @@ class BankAccountController extends Controller
]);
}
public function update(int $id): Response
public function update(Request $request, string $id): Response
{
$this->authorize('accounting.bank_account.manage');
$account = BankAccount::findOrFail($id);
$account = BankAccount::findOrFail((int) $id);
$data = $this->validate($_POST, [
'account_name_ar' => 'required',
......
......@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\BankReconciliation;
......@@ -67,11 +68,11 @@ class BankReconciliationController extends Controller
return $this->redirect('/accounting/bank-reconciliation/create');
}
public function show(int $id): Response
public function show(Request $request, string $id): Response
{
$this->authorize('accounting.bank_recon.view');
$recon = BankReconciliation::findOrFail($id);
$recon = BankReconciliation::findOrFail((int) $id);
$items = $recon->items();
$bankAccount = BankAccount::find((int) $recon->bank_account_id);
......@@ -88,7 +89,7 @@ class BankReconciliationController extends Controller
]);
}
public function addItem(int $id): Response
public function addItem(Request $request, string $id): Response
{
$this->authorize('accounting.bank_recon.manage');
......@@ -98,7 +99,7 @@ class BankReconciliationController extends Controller
'amount' => 'required|numeric',
]);
$result = BankReconciliationService::addItem($id, $_POST);
$result = BankReconciliationService::addItem((int) $id, $_POST);
$session = App::getInstance()->session();
if ($result['success']) {
......@@ -110,11 +111,11 @@ class BankReconciliationController extends Controller
return $this->redirect('/accounting/bank-reconciliation/' . $id);
}
public function complete(int $id): Response
public function complete(Request $request, string $id): Response
{
$this->authorize('accounting.bank_recon.manage');
$result = BankReconciliationService::complete($id);
$result = BankReconciliationService::complete((int) $id);
$session = App::getInstance()->session();
if ($result['success']) {
......
......@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\Account;
......@@ -89,16 +90,16 @@ class ChartOfAccountsController extends Controller
return $this->redirect('/accounting/chart-of-accounts');
}
public function edit(int $id): Response
public function edit(Request $request, string $id): Response
{
$this->authorize('accounting.coa.manage');
$account = Account::findOrFail($id);
$account = Account::findOrFail((int) $id);
$parentAccounts = Account::query()
->where('is_archived', '=', 0)
->where('is_header', '=', 1)
->where('id', '!=', $id)
->where('id', '!=', (int) $id)
->orderBy('account_code', 'ASC')
->get();
......@@ -111,11 +112,11 @@ class ChartOfAccountsController extends Controller
]);
}
public function update(int $id): Response
public function update(Request $request, string $id): Response
{
$this->authorize('accounting.coa.manage');
$account = Account::findOrFail($id);
$account = Account::findOrFail((int) $id);
$data = $this->validate($_POST, [
'name_ar' => 'required',
......@@ -128,7 +129,7 @@ class ChartOfAccountsController extends Controller
if ((int) $account->is_system === 1 && ($_POST['account_code'] ?? '') !== $account->account_code) {
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => 'لا يمكن تعديل كود حساب نظامي']]);
return $this->redirect('/accounting/chart-of-accounts/' . $id . '/edit');
return $this->redirect('/accounting/chart-of-accounts/' . (int) $id . '/edit');
}
$account->update([
......
......@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\CostCenter;
......@@ -80,18 +81,18 @@ class CostCenterController extends Controller
return $this->redirect('/accounting/cost-centers');
}
public function edit(int $id): Response
public function edit(Request $request, string $id): Response
{
$this->authorize('accounting.cost_center.manage');
$center = CostCenter::findOrFail($id);
$center = CostCenter::findOrFail((int) $id);
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar, name_en FROM branches WHERE is_active = 1 ORDER BY name_ar");
$parents = CostCenter::query()
->where('is_archived', '=', 0)
->where('id', '!=', $id)
->where('id', '!=', (int) $id)
->orderBy('code', 'ASC')
->get();
......@@ -102,11 +103,11 @@ class CostCenterController extends Controller
]);
}
public function update(int $id): Response
public function update(Request $request, string $id): Response
{
$this->authorize('accounting.cost_center.manage');
$center = CostCenter::findOrFail($id);
$center = CostCenter::findOrFail((int) $id);
$data = $this->validate($_POST, [
'name_ar' => 'required',
......
......@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\FiscalYear;
......@@ -92,12 +93,12 @@ class FiscalYearController extends Controller
return $this->redirect('/accounting/fiscal-years');
}
public function show(int $id): Response
public function show(Request $request, string $id): Response
{
$this->authorize('accounting.fiscal_year.view');
$fy = FiscalYear::findOrFail($id);
$periods = PeriodClosingService::getPeriodSummary($id);
$fy = FiscalYear::findOrFail((int) $id);
$periods = PeriodClosingService::getPeriodSummary((int) $id);
return $this->view('Accounting/Views/fiscal_years/show', [
'fiscal_year' => $fy->toArray(),
......@@ -105,11 +106,11 @@ class FiscalYearController extends Controller
]);
}
public function close(int $id): Response
public function close(Request $request, string $id): Response
{
$this->authorize('accounting.fiscal_year.close');
$result = PeriodClosingService::closeFiscalYear($id);
$result = PeriodClosingService::closeFiscalYear((int) $id);
$session = App::getInstance()->session();
if ($result['success']) {
......@@ -118,6 +119,6 @@ class FiscalYearController extends Controller
$session->flash('_alerts', [['type' => 'error', 'message' => $result['error']]]);
}
return $this->redirect('/accounting/fiscal-years/' . $id);
return $this->redirect('/accounting/fiscal-years/' . (int) $id);
}
}
......@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\App;
use App\Core\Response;
use App\Modules\Accounting\Models\JournalEntry;
......@@ -116,12 +117,12 @@ class JournalEntryController extends Controller
return $this->redirect('/accounting/journal-entries/create');
}
public function show(int $id): Response
public function show(Request $request, string $id): Response
{
$this->authorize('accounting.journal.view');
$entry = JournalEntry::findOrFail($id);
$lines = JournalEntryLine::getByJournalEntry($id);
$entry = JournalEntry::findOrFail((int) $id);
$lines = JournalEntryLine::getByJournalEntry((int) $id);
// Get employee names
$db = App::getInstance()->db();
......@@ -142,11 +143,11 @@ class JournalEntryController extends Controller
]);
}
public function post(int $id): Response
public function post(Request $request, string $id): Response
{
$this->authorize('accounting.journal.post');
$result = JournalService::postEntry($id);
$result = JournalService::postEntry((int) $id);
$session = App::getInstance()->session();
if ($result['success']) {
......@@ -155,16 +156,16 @@ class JournalEntryController extends Controller
$session->flash('_alerts', [['type' => 'error', 'message' => $result['error']]]);
}
return $this->redirect('/accounting/journal-entries/' . $id);
return $this->redirect('/accounting/journal-entries/' . (int) $id);
}
public function reverse(int $id): Response
public function reverse(Request $request, string $id): Response
{
$this->authorize('accounting.journal.reverse');
$reason = $_POST['reason'] ?? 'عكس قيد';
$reason = $request->post('reason', 'عكس قيد');
$result = JournalService::reverseEntry($id, $reason);
$result = JournalService::reverseEntry((int) $id, $reason);
$session = App::getInstance()->session();
if ($result['success']) {
......@@ -173,7 +174,7 @@ class JournalEntryController extends Controller
$session->flash('_alerts', [['type' => 'error', 'message' => $result['error']]]);
}
return $this->redirect('/accounting/journal-entries/' . $id);
return $this->redirect('/accounting/journal-entries/' . (int) $id);
}
public function searchAccounts(): Response
......
......@@ -180,11 +180,24 @@ final class PeriodClosingService
}
// Check all monthly periods are closed
$openPeriods = $db->selectOne(
"SELECT COUNT(*) as cnt FROM period_closings
WHERE fiscal_year_id = ? AND status != 'closed' AND is_archived = 0",
[$fiscalYearId]
);
$startDate = $fiscalYear->start_date;
$endDate = $fiscalYear->end_date;
$start = new \DateTime($startDate);
$end = new \DateTime($endDate);
$unclosedPeriods = [];
while ($start <= $end) {
$period = $start->format('Y-m');
if (!PeriodClosing::isPeriodClosed($fiscalYearId, $period)) {
$unclosedPeriods[] = $period;
}
$start->modify('first day of next month');
}
if (!empty($unclosedPeriods)) {
return [
'success' => false,
'error' => 'يوجد فترات غير مغلقة: ' . implode(', ', $unclosedPeriods) . ' — يجب إغلاقها أولاً',
];
}
// Get income statement for the entire year
$incomeStatement = FinancialReportService::getIncomeStatement(
......@@ -202,8 +215,6 @@ final class PeriodClosingService
return ['success' => false, 'error' => 'حساب الأرباح المحتجزة (3102) غير موجود'];
}
$db->beginTransaction();
try {
$lines = [];
// Close revenue accounts (debit revenue accounts to zero them out)
......@@ -253,6 +264,7 @@ final class PeriodClosingService
return ['success' => false, 'error' => 'لا توجد حسابات إيرادات أو مصروفات لإقفالها'];
}
// Step 1: Create closing journal entry (JournalService manages its own transaction)
$closingResult = JournalService::createEntry([
'entry_date' => $fiscalYear->end_date,
'description_ar' => 'قيد إقفال السنة المالية ' . $fiscalYear->name_ar,
......@@ -263,11 +275,12 @@ final class PeriodClosingService
], $lines, true);
if (!$closingResult['success']) {
$db->rollBack();
return $closingResult;
}
// Update fiscal year status
// Step 2: Update fiscal year status and create year-end period record
$db->beginTransaction();
try {
$db->update('fiscal_years', [
'status' => 'closed',
'is_current' => 0,
......@@ -276,7 +289,6 @@ final class PeriodClosingService
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$fiscalYearId]);
// Create year-end period closing record
$endPeriod = substr($fiscalYear->end_date, 0, 7);
$db->insert('period_closings', [
'fiscal_year_id' => $fiscalYearId,
......@@ -294,6 +306,11 @@ final class PeriodClosingService
]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
Logger::error("Fiscal year status update failed (closing entry already posted): " . $e->getMessage());
return ['success' => false, 'error' => 'تم ترحيل قيد الإقفال لكن فشل تحديث حالة السنة المالية: ' . $e->getMessage()];
}
EventBus::dispatch('accounting.fiscal_year.closed', [
'fiscal_year_id' => $fiscalYearId,
......@@ -312,11 +329,6 @@ final class PeriodClosingService
'closing_entry_id' => $closingResult['journal_entry_id'],
'closing_entry_num' => $closingResult['entry_number'],
];
} catch (\Throwable $e) {
$db->rollBack();
Logger::error("Fiscal year closing failed: " . $e->getMessage());
return ['success' => false, 'error' => 'فشل إقفال السنة المالية: ' . $e->getMessage()];
}
}
/**
......
......@@ -50,7 +50,7 @@
</thead>
<tbody>
<?php foreach ($outstanding as $ap): ?>
<?php $isOverdue = strtotime($ap['due_date']) < time(); ?>
<?php $isOverdue = !empty($ap['due_date']) && strtotime($ap['due_date']) < time(); ?>
<tr>
<td><?= e($ap['supplier_name'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;"><?= e($ap['invoice_number']) ?></td>
......
......@@ -60,7 +60,7 @@
'membership_fee' => 'عضوية',
default => $ar['document_type'] ?? '—',
};
$isOverdue = strtotime($ar['due_date']) < time();
$isOverdue = !empty($ar['due_date']) && strtotime($ar['due_date']) < time();
?>
<tr>
<td><?= e($ar['member_name'] ?? '—') ?></td>
......
......@@ -9,7 +9,7 @@
<form method="GET" action="/accounting/reports/general-ledger" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="flex:2;">
<label class="form-label" style="font-size:12px;">الحساب</label>
<input type="number" name="account_id" class="form-input" value="<?= $account_id ?>" placeholder="رقم الحساب (ID)">
<input type="number" name="account_id" class="form-input" value="<?= e($account_id) ?>" placeholder="رقم الحساب (ID)">
</div>
<div>
<label class="form-label" style="font-size:12px;">من تاريخ</label>
......
......@@ -9,7 +9,7 @@
<form method="GET" action="/accounting/reports/member-statement" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">رقم العضو</label>
<input type="number" name="member_id" class="form-input" value="<?= $member_id ?>" placeholder="ID العضو">
<input type="number" name="member_id" class="form-input" value="<?= e($member_id) ?>" placeholder="ID العضو">
</div>
<div>
<label class="form-label" style="font-size:12px;">من تاريخ</label>
......
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