From 9108deb2d9e15ffb33b22fc59ad8b64990630a10 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 3 May 2026 02:08:14 +0000 Subject: [PATCH] update accounts --- .../20260503_seed_chart_of_accounts.php | 9 + includes/accounting_helper.php | 170 ++++++++++++++++++ index.php | 17 +- pages/accounting_logic.php | 64 +++++-- pages/accounting_view.php | 82 +++++++-- 5 files changed, 309 insertions(+), 33 deletions(-) create mode 100644 db/migrations/20260503_seed_chart_of_accounts.php diff --git a/db/migrations/20260503_seed_chart_of_accounts.php b/db/migrations/20260503_seed_chart_of_accounts.php new file mode 100644 index 0000000..f74fccd --- /dev/null +++ b/db/migrations/20260503_seed_chart_of_accounts.php @@ -0,0 +1,9 @@ +prepare( + "SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? LIMIT 1" + ); + $stmt->execute([$tableName]); + $cache[$normalized] = (bool)$stmt->fetchColumn(); + } catch (Throwable $e) { + error_log('Accounting table check failed: ' . $e->getMessage()); + $cache[$normalized] = false; + } + + return $cache[$normalized]; +} + +function getDefaultAccountingAccounts(): array { + return [ + ['code' => '1000', 'name_en' => 'Assets', 'name_ar' => 'الأصول', 'type' => 'asset', 'parent_code' => null], + ['code' => '1100', 'name_en' => 'Cash on Hand', 'name_ar' => 'النقدية بالصندوق', 'type' => 'asset', 'parent_code' => '1000'], + ['code' => '1110', 'name_en' => 'Petty Cash', 'name_ar' => 'العهدة النقدية', 'type' => 'asset', 'parent_code' => '1000'], + ['code' => '1200', 'name_en' => 'Bank Account', 'name_ar' => 'حساب البنك', 'type' => 'asset', 'parent_code' => '1000'], + ['code' => '1300', 'name_en' => 'Accounts Receivable', 'name_ar' => 'ذمم العملاء', 'type' => 'asset', 'parent_code' => '1000'], + ['code' => '1400', 'name_en' => 'Inventory', 'name_ar' => 'المخزون', 'type' => 'asset', 'parent_code' => '1000'], + ['code' => '1500', 'name_en' => 'VAT Input', 'name_ar' => 'ضريبة المدخلات', 'type' => 'asset', 'parent_code' => '1000'], + ['code' => '1600', 'name_en' => 'Fixed Assets', 'name_ar' => 'الأصول الثابتة', 'type' => 'asset', 'parent_code' => '1000'], + ['code' => '1700', 'name_en' => 'Prepaid Expenses', 'name_ar' => 'المصروفات المقدمة', 'type' => 'asset', 'parent_code' => '1000'], + ['code' => '2000', 'name_en' => 'Liabilities', 'name_ar' => 'الالتزامات', 'type' => 'liability', 'parent_code' => null], + ['code' => '2100', 'name_en' => 'Accounts Payable', 'name_ar' => 'ذمم الموردين', 'type' => 'liability', 'parent_code' => '2000'], + ['code' => '2200', 'name_en' => 'Payroll Liabilities', 'name_ar' => 'التزامات الرواتب', 'type' => 'liability', 'parent_code' => '2000'], + ['code' => '2300', 'name_en' => 'VAT Payable', 'name_ar' => 'ضريبة المخرجات', 'type' => 'liability', 'parent_code' => '2000'], + ['code' => '2400', 'name_en' => 'Accrued Expenses', 'name_ar' => 'مصروفات مستحقة', 'type' => 'liability', 'parent_code' => '2000'], + ['code' => '3000', 'name_en' => 'Equity', 'name_ar' => 'حقوق الملكية', 'type' => 'equity', 'parent_code' => null], + ['code' => '3100', 'name_en' => 'Owner Capital', 'name_ar' => 'رأس المال', 'type' => 'equity', 'parent_code' => '3000'], + ['code' => '3200', 'name_en' => 'Retained Earnings', 'name_ar' => 'الأرباح المحتجزة', 'type' => 'equity', 'parent_code' => '3000'], + ['code' => '4000', 'name_en' => 'Revenue', 'name_ar' => 'الإيرادات', 'type' => 'revenue', 'parent_code' => null], + ['code' => '4100', 'name_en' => 'Sales Revenue', 'name_ar' => 'إيرادات المبيعات', 'type' => 'revenue', 'parent_code' => '4000'], + ['code' => '4200', 'name_en' => 'Service Revenue', 'name_ar' => 'إيرادات الخدمات', 'type' => 'revenue', 'parent_code' => '4000'], + ['code' => '5000', 'name_en' => 'Expenses', 'name_ar' => 'المصروفات', 'type' => 'expense', 'parent_code' => null], + ['code' => '5100', 'name_en' => 'Cost of Goods Sold', 'name_ar' => 'تكلفة البضاعة المباعة', 'type' => 'expense', 'parent_code' => '5000'], + ['code' => '5200', 'name_en' => 'Operating Expenses', 'name_ar' => 'المصروفات التشغيلية', 'type' => 'expense', 'parent_code' => '5000'], + ['code' => '5300', 'name_en' => 'Payroll Expenses', 'name_ar' => 'مصروفات الرواتب', 'type' => 'expense', 'parent_code' => '5000'], + ['code' => '5400', 'name_en' => 'Rent Expense', 'name_ar' => 'مصروف الإيجار', 'type' => 'expense', 'parent_code' => '5000'], + ['code' => '5500', 'name_en' => 'Utilities Expense', 'name_ar' => 'مصروف المرافق', 'type' => 'expense', 'parent_code' => '5000'], + ['code' => '5600', 'name_en' => 'Marketing Expense', 'name_ar' => 'مصروف التسويق', 'type' => 'expense', 'parent_code' => '5000'], + ['code' => '5700', 'name_en' => 'Transport Expense', 'name_ar' => 'مصروف النقل', 'type' => 'expense', 'parent_code' => '5000'], + ]; +} + +function seedDefaultAccountingAccounts(): int { + if (!accountingTableExists('acc_accounts')) { + return 0; + } + + $db = db(); + $accounts = getDefaultAccountingAccounts(); + $inserted = 0; + $startedTransaction = false; + + try { + if (!$db->inTransaction()) { + $db->beginTransaction(); + $startedTransaction = true; + } + + $selectId = $db->prepare("SELECT id FROM acc_accounts WHERE code = ? LIMIT 1"); + $insert = $db->prepare( + "INSERT INTO acc_accounts (code, name_en, name_ar, type, parent_id) VALUES (?, ?, ?, ?, NULL)" + ); + $updateParent = $db->prepare("UPDATE acc_accounts SET parent_id = ? WHERE code = ?"); + + foreach ($accounts as $account) { + $selectId->execute([$account['code']]); + if (!$selectId->fetchColumn()) { + $insert->execute([ + $account['code'], + $account['name_en'], + $account['name_ar'], + $account['type'], + ]); + $inserted++; + } + } + + foreach ($accounts as $account) { + if (empty($account['parent_code'])) { + continue; + } + + $selectId->execute([$account['parent_code']]); + $parentId = $selectId->fetchColumn(); + if ($parentId) { + $updateParent->execute([(int)$parentId, $account['code']]); + } + } + + if ($startedTransaction) { + $db->commit(); + } + } catch (Throwable $e) { + if ($startedTransaction && $db->inTransaction()) { + $db->rollBack(); + } + error_log('Accounting seed failed: ' . $e->getMessage()); + return 0; + } + + return $inserted; +} + +function createAccountingAccount(string $code, string $nameEn, string $nameAr, string $type, ?int $parentId = null): array { + if (!accountingTableExists('acc_accounts')) { + return ['success' => false, 'error' => 'Accounting tables are not ready yet.']; + } + + $code = strtoupper(trim($code)); + $nameEn = trim($nameEn); + $nameAr = trim($nameAr); + $allowedTypes = ['asset', 'liability', 'equity', 'revenue', 'expense']; + + if ($code === '' || !preg_match('/^[A-Z0-9._-]{1,20}$/', $code)) { + return ['success' => false, 'error' => 'Please enter a valid unique account code.']; + } + if ($nameEn === '' || $nameAr === '') { + return ['success' => false, 'error' => 'English and Arabic account names are required.']; + } + if (!in_array($type, $allowedTypes, true)) { + return ['success' => false, 'error' => 'Invalid account type selected.']; + } + + $db = db(); + + try { + if ($parentId !== null) { + $stmtParent = $db->prepare("SELECT id, type FROM acc_accounts WHERE id = ? LIMIT 1"); + $stmtParent->execute([$parentId]); + $parent = $stmtParent->fetch(PDO::FETCH_ASSOC); + + if (!$parent) { + return ['success' => false, 'error' => 'Selected parent account was not found.']; + } + if (($parent['type'] ?? '') !== $type) { + return ['success' => false, 'error' => 'Parent and child accounts must use the same type.']; + } + } + + $stmtExists = $db->prepare("SELECT id FROM acc_accounts WHERE code = ? LIMIT 1"); + $stmtExists->execute([$code]); + if ($stmtExists->fetchColumn()) { + return ['success' => false, 'error' => 'That account code already exists.']; + } + + $stmt = $db->prepare( + "INSERT INTO acc_accounts (code, name_en, name_ar, type, parent_id) VALUES (?, ?, ?, ?, ?)" + ); + $stmt->execute([$code, $nameEn, $nameAr, $type, $parentId ?: null]); + + return ['success' => true, 'id' => (int)$db->lastInsertId()]; + } catch (Throwable $e) { + error_log('Accounting account create failed: ' . $e->getMessage()); + return ['success' => false, 'error' => 'Unable to save the account right now.']; + } +} + function createJournalEntry($date, $description, $reference, $source_type, $source_id, $items) { $db = db(); try { diff --git a/index.php b/index.php index a6e3cf7..02a208c 100644 --- a/index.php +++ b/index.php @@ -5154,9 +5154,12 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
- + + + Accounts + @@ -9148,7 +9151,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; - OMR + OMR @@ -9416,10 +9419,10 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; - OMR - + OMR - - OMR - OMR + OMR + + OMR + - OMR + OMR @@ -9463,7 +9466,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
diff --git a/pages/accounting_logic.php b/pages/accounting_logic.php index bdc137a..9304980 100644 --- a/pages/accounting_logic.php +++ b/pages/accounting_logic.php @@ -1,12 +1,53 @@ 0 + ? "Main chart of accounts seeded successfully! {$added} account(s) added." + : 'Main chart of accounts already exists. Parent links were checked.'; + + redirectWithMessage($message, 'index.php?page=accounting&view=coa'); + } + + if (isset($_POST['add_account'])) { + $parentId = isset($_POST['parent_id']) && $_POST['parent_id'] !== '' ? (int)$_POST['parent_id'] : null; + $result = createAccountingAccount( + $_POST['code'] ?? '', + $_POST['name_en'] ?? '', + $_POST['name_ar'] ?? '', + $_POST['type'] ?? '', + $parentId + ); + + if (!empty($result['success'])) { + redirectWithMessage('Account added successfully!', 'index.php?page=accounting&view=coa'); + } + + redirectWithMessage((string)($result['error'] ?? 'Unable to save the account right now.'), 'index.php?page=accounting&view=coa'); + } +} + +if (isset($_GET['action']) && $_GET['action'] === 'get_entry_details') { + header('Content-Type: application/json'); + $id = (int)($_GET['id'] ?? 0); + $stmt = db()->prepare( + "SELECT l.*, a.name_en, a.code FROM acc_ledger l JOIN acc_accounts a ON l.account_id = a.id WHERE l.journal_entry_id = ?" + ); + $stmt->execute([$id]); + echo json_encode($stmt->fetchAll()); + exit; +} + $page_num = isset($_GET['p']) && is_numeric($_GET['p']) ? (int)$_GET['p'] : 1; $limit = 50; $offset = ($page_num - 1) * $limit; -// Count total entries for pagination -$total_entries = db()->query("SELECT COUNT(*) FROM acc_journal_entries")->fetchColumn(); -$data['total_pages'] = ceil($total_entries / $limit); +$total_entries = (int)db()->query("SELECT COUNT(*) FROM acc_journal_entries")->fetchColumn(); +$data['total_pages'] = max(1, (int)ceil($total_entries / $limit)); $data['current_page'] = $page_num; $data['journal_entries'] = db()->query("SELECT je.*, @@ -15,15 +56,6 @@ $data['journal_entries'] = db()->query("SELECT je.*, ORDER BY je.entry_date DESC, je.id DESC LIMIT $limit OFFSET $offset")->fetchAll(); $data['accounts'] = db()->query("SELECT * FROM acc_accounts ORDER BY code ASC")->fetchAll(); -if (isset($_GET['action']) && $_GET['action'] === 'get_entry_details') { - header('Content-Type: application/json'); - $id = (int)$_GET['id']; - $stmt = db()->prepare("SELECT l.*, a.name_en, a.code FROM acc_ledger l JOIN acc_accounts a ON l.account_id = a.id WHERE l.journal_entry_id = ?"); - $stmt->execute([$id]); - echo json_encode($stmt->fetchAll()); - exit; -} - if (isset($_GET['view']) && $_GET['view'] === 'trial_balance') { $data['trial_balance'] = db()->query("SELECT a.code, a.name_en, SUM(l.debit) as total_debit, SUM(l.credit) as total_credit FROM acc_accounts a @@ -57,4 +89,10 @@ if (isset($_GET['view']) && $_GET['view'] === 'coa') { FROM acc_accounts a LEFT JOIN acc_accounts p ON a.parent_id = p.id ORDER BY a.code ASC")->fetchAll(); + + $data['coa_summary'] = [ + 'total' => count($data['coa']), + 'parents' => count(array_filter($data['coa'], static fn($account) => empty($account['parent_id']))), + 'children' => count(array_filter($data['coa'], static fn($account) => !empty($account['parent_id']))), + ]; } diff --git a/pages/accounting_view.php b/pages/accounting_view.php index 309900b..63a833a 100644 --- a/pages/accounting_view.php +++ b/pages/accounting_view.php @@ -12,7 +12,7 @@
Journal - Accounts + Chart of Accounts Trial Balance P&L Balance Sheet @@ -31,7 +31,7 @@

-

+

@@ -68,18 +68,68 @@ -
- +
+
+
+
+
Total Accounts
+
+
All parent and child accounts in your chart.
+
+
+
+
+
+
+
Main Parents
+
+
Top-level groups like Assets, Liabilities, Revenue, and Expenses.
+
+
+
+
+
+
+
Child Accounts
+
+
Operational accounts you can post transactions into.
+
+
+
+ +
+
+
Chart of Accounts
+

Use this page to seed the main parent accounts, then add custom child accounts under the right parent.

+
+
+
+ +
+ +
+
+ + +
+ + No accounts exist yet. Click Seed Main Accounts to create the default chart of accounts. +
+ +
- +
+ @@ -87,21 +137,27 @@ - + - - - + + + +
Code Name TypeLevel Parent Balance

+ + Main + + Child + +
- - +