diff --git a/db/migrations/023_translate_expense_categories.sql b/db/migrations/023_translate_expense_categories.sql new file mode 100644 index 0000000..45a529c --- /dev/null +++ b/db/migrations/023_translate_expense_categories.sql @@ -0,0 +1,12 @@ +-- Translate default expense categories to Arabic + +UPDATE expense_categories SET name = 'مستلزمات مكتبية', description = 'قرطاسية، أحبار، ورق، إلخ' WHERE name = 'Office Supplies'; +UPDATE expense_categories SET name = 'سفر وتنقلات', description = 'تذاكر طيران، فنادق، مواصلات' WHERE name = 'Travel'; +UPDATE expense_categories SET name = 'وجبات وضيافة', description = 'غداء عمل، فعاليات الفريق' WHERE name = 'Meals & Entertainment'; +UPDATE expense_categories SET name = 'مرافق وخدمات', description = 'كهرباء، مياه، انترنت' WHERE name = 'Utilities'; +UPDATE expense_categories SET name = 'إيجار', description = 'إيجار المكتب' WHERE name = 'Rent'; +UPDATE expense_categories SET name = 'رواتب وأجور', description = 'رواتب الموظفين' WHERE name = 'Salaries'; +UPDATE expense_categories SET name = 'صيانة وإصلاحات', description = 'صيانة دورية وإصلاح أعطال' WHERE name = 'Maintenance'; +UPDATE expense_categories SET name = 'تسويق وإعلان', description = 'حملات إعلانية وترويج' WHERE name = 'Advertising'; +UPDATE expense_categories SET name = 'برمجيات وتراخيص', description = 'اشتراكات برامج وتراخيص' WHERE name = 'Software'; +UPDATE expense_categories SET name = 'مصروفات أخرى', description = 'نفقات متنوعة غير مصنفة' WHERE name = 'Other'; diff --git a/db/migrations/024_link_expenses_to_accounting.sql b/db/migrations/024_link_expenses_to_accounting.sql new file mode 100644 index 0000000..b03b89e --- /dev/null +++ b/db/migrations/024_link_expenses_to_accounting.sql @@ -0,0 +1,23 @@ +-- Add account_id to expense_categories +ALTER TABLE expense_categories ADD COLUMN account_id INT DEFAULT NULL; +ALTER TABLE expense_categories ADD CONSTRAINT fk_expense_category_account FOREIGN KEY (account_id) REFERENCES accounting_accounts(id) ON DELETE SET NULL; + +-- Seed default mappings (Best effort based on Arabic account names vs English categories) +-- Accounts: +-- 13: مصروفات الرواتب (Salaries) +-- 16: مصروفات التسويق (Marketing) +-- 30: مصروفات الإيجار (Rent) +-- 31: مصروفات المرافق (Utilities) +-- 12: تكلفة البضاعة المباعة (COGS) + +-- Update 'Salaries' category +UPDATE expense_categories SET account_id = 13 WHERE name LIKE '%Salaries%' OR name LIKE '%رواتب%'; + +-- Update 'Rent' category +UPDATE expense_categories SET account_id = 30 WHERE name LIKE '%Rent%' OR name LIKE '%إيجار%'; + +-- Update 'Utilities' category +UPDATE expense_categories SET account_id = 31 WHERE name LIKE '%Utilities%' OR name LIKE '%مرافق%'; + +-- Update 'Advertising' category +UPDATE expense_categories SET account_id = 16 WHERE name LIKE '%Advertising%' OR name LIKE '%Marketing%' OR name LIKE '%تسو%'; diff --git a/db/migrations/025_add_journal_tracking.sql b/db/migrations/025_add_journal_tracking.sql new file mode 100644 index 0000000..256ca70 --- /dev/null +++ b/db/migrations/025_add_journal_tracking.sql @@ -0,0 +1,6 @@ +-- Add journal_id to expenses and hr_payroll for tracking +ALTER TABLE expenses ADD COLUMN journal_id INT DEFAULT NULL; +ALTER TABLE expenses ADD CONSTRAINT fk_expense_journal FOREIGN KEY (journal_id) REFERENCES accounting_journal(id) ON DELETE SET NULL; + +ALTER TABLE hr_payroll ADD COLUMN journal_id INT DEFAULT NULL; +ALTER TABLE hr_payroll ADD CONSTRAINT fk_payroll_journal FOREIGN KEY (journal_id) REFERENCES accounting_journal(id) ON DELETE SET NULL; diff --git a/db/migrations/026_fix_accounting_accounts_type.sql b/db/migrations/026_fix_accounting_accounts_type.sql new file mode 100644 index 0000000..d56dab6 --- /dev/null +++ b/db/migrations/026_fix_accounting_accounts_type.sql @@ -0,0 +1,2 @@ +-- Fix accounting_accounts type column length +ALTER TABLE accounting_accounts MODIFY type VARCHAR(100) NOT NULL; diff --git a/db/migrations/fix_accounting_accounts_type.php b/db/migrations/fix_accounting_accounts_type.php deleted file mode 100644 index 7a65822..0000000 --- a/db/migrations/fix_accounting_accounts_type.php +++ /dev/null @@ -1,12 +0,0 @@ -exec("ALTER TABLE accounting_accounts MODIFY type VARCHAR(100) NOT NULL"); - echo "Successfully updated accounting_accounts table structure."; -} catch (PDOException $e) { - echo "Error: " . $e->getMessage(); -} -?> diff --git a/expense_categories.php b/expense_categories.php index d2e82f2..71a98a5 100644 --- a/expense_categories.php +++ b/expense_categories.php @@ -15,17 +15,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $id = $_POST['id'] ?? 0; $name = trim($_POST['name'] ?? ''); $description = trim($_POST['description'] ?? ''); + $account_id = !empty($_POST['account_id']) ? $_POST['account_id'] : null; if ($name) { try { $db = db(); if ($action === 'add') { - $stmt = $db->prepare("INSERT INTO expense_categories (name, description) VALUES (?, ?)"); - $stmt->execute([$name, $description]); + $stmt = $db->prepare("INSERT INTO expense_categories (name, description, account_id) VALUES (?, ?, ?)"); + $stmt->execute([$name, $description, $account_id]); $_SESSION['success'] = 'تم إضافة التصنيف بنجاح'; } elseif ($action === 'edit' && $id) { - $stmt = $db->prepare("UPDATE expense_categories SET name = ?, description = ? WHERE id = ?"); - $stmt->execute([$name, $description, $id]); + $stmt = $db->prepare("UPDATE expense_categories SET name = ?, description = ?, account_id = ? WHERE id = ?"); + $stmt->execute([$name, $description, $account_id, $id]); $_SESSION['success'] = 'تم تحديث التصنيف بنجاح'; } redirect('expense_categories.php'); @@ -55,7 +56,8 @@ if (isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['id']) redirect('expense_categories.php'); } -$categories = db()->query("SELECT * FROM expense_categories ORDER BY name")->fetchAll(PDO::FETCH_ASSOC); +$categories = db()->query("SELECT c.*, a.name as account_name FROM expense_categories c LEFT JOIN accounting_accounts a ON c.account_id = a.id ORDER BY c.name")->fetchAll(PDO::FETCH_ASSOC); +$accounts = db()->query("SELECT * FROM accounting_accounts ORDER BY type, name")->fetchAll(PDO::FETCH_ASSOC); if (isset($_SESSION['success'])) { $success = $_SESSION['success']; @@ -97,6 +99,7 @@ if (isset($_SESSION['error'])) { اسم التصنيف + الحساب المحاسبي المرتبط الوصف الإجراءات @@ -105,6 +108,13 @@ if (isset($_SESSION['error'])) { + + + + + غير مرتبط + + @@ -143,6 +153,19 @@ if (isset($_SESSION['error'])) { + +
+ + +
عند تسجيل مصروف بهذا التصنيف، سيتم إنشاء قيد محاسبي تلقائياً على هذا الحساب.
+
@@ -174,11 +197,13 @@ function openModal(action, data = null) { document.getElementById('modalId').value = 0; document.getElementById('modalName').value = ''; document.getElementById('modalDescription').value = ''; + document.getElementById('modalAccountId').value = ''; } else { title.textContent = 'تعديل التصنيف'; document.getElementById('modalId').value = data.id; document.getElementById('modalName').value = data.name; document.getElementById('modalDescription').value = data.description; + document.getElementById('modalAccountId').value = data.account_id || ''; } categoryModal.show(); diff --git a/expenses.php b/expenses.php index 9d753d8..2eb3980 100644 --- a/expenses.php +++ b/expenses.php @@ -1,5 +1,6 @@ prepare("SELECT a.name as account_name FROM expense_categories c LEFT JOIN accounting_accounts a ON c.account_id = a.id WHERE c.id = ?"); + $stmt_cat->execute([$category_id]); + $cat_account = $stmt_cat->fetchColumn(); + + // 2. Get Payment Account (Default: Cash / النقدية) + // Ideally, map Payment Method to Account. For now, defaulting to 'النقدية'. + $pay_account = 'النقدية'; + // Could be improved: if ($payment_method == 'Bank Transfer') $pay_account = 'Bank'; etc. + if ($action === 'add') { + // Create Expense $stmt = $db->prepare("INSERT INTO expenses (date, category_id, amount, description, reference, vendor, payment_method, receipt_file, user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); $stmt->execute([$date, $category_id, $amount, $description, $reference, $vendor, $payment_method, $receipt_path, $_SESSION['user_id']]); - $_SESSION['success'] = 'تم إضافة المصروف بنجاح'; - } else { - // Get old file if not replaced - if (!$receipt_path) { - $stmt = $db->prepare("SELECT receipt_file FROM expenses WHERE id = ?"); - $stmt->execute([$id]); - $receipt_path = $stmt->fetchColumn(); + $expense_id = $db->lastInsertId(); + + // Create Journal Entry + if ($cat_account) { + $entries = [ + ['account' => $cat_account, 'debit' => $amount, 'credit' => 0], + ['account' => $pay_account, 'debit' => 0, 'credit' => $amount] + ]; + $journal_desc = "Expense #$expense_id: " . ($vendor ? "$vendor - " : "") . $description; + $jid = add_journal_entry($date, $journal_desc, $reference, $entries); + + if ($jid) { + $db->prepare("UPDATE expenses SET journal_id = ? WHERE id = ?")->execute([$jid, $expense_id]); + } } + $_SESSION['success'] = 'تم إضافة المصروف بنجاح' . ($cat_account ? ' وتم إنشاء قيد محاسبي.' : ''); + } else { + // Update Expense + // Get existing journal_id and file + $stmt = $db->prepare("SELECT journal_id, receipt_file FROM expenses WHERE id = ?"); + $stmt->execute([$id]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + $journal_id = $row['journal_id'] ?? null; + $old_file = $row['receipt_file'] ?? null; + + if (!$receipt_path) $receipt_path = $old_file; + $stmt = $db->prepare("UPDATE expenses SET date=?, category_id=?, amount=?, description=?, reference=?, vendor=?, payment_method=?, receipt_file=? WHERE id=?"); $stmt->execute([$date, $category_id, $amount, $description, $reference, $vendor, $payment_method, $receipt_path, $id]); + + // Update Journal Entry + if ($cat_account) { + $entries = [ + ['account' => $cat_account, 'debit' => $amount, 'credit' => 0], + ['account' => $pay_account, 'debit' => 0, 'credit' => $amount] + ]; + $journal_desc = "Expense #$id: " . ($vendor ? "$vendor - " : "") . $description; + + if ($journal_id) { + edit_journal_entry($journal_id, $date, $journal_desc, $reference, $entries); + } else { + // Create new if missing + $jid = add_journal_entry($date, $journal_desc, $reference, $entries); + if ($jid) { + $db->prepare("UPDATE expenses SET journal_id = ? WHERE id = ?")->execute([$jid, $id]); + } + } + } + $_SESSION['success'] = 'تم تحديث المصروف بنجاح'; } redirect('expenses.php'); @@ -70,14 +124,24 @@ if (isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['id']) if (!canDelete('expenses')) redirect('expenses.php'); $id = $_GET['id']; $db = db(); - // Get file to delete - $stmt = $db->prepare("SELECT receipt_file FROM expenses WHERE id = ?"); - $stmt->execute([$id]); - $file = $stmt->fetchColumn(); - if ($file && file_exists($file)) unlink($file); + // Get file and journal_id + $stmt = $db->prepare("SELECT receipt_file, journal_id FROM expenses WHERE id = ?"); + $stmt->execute([$id]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + // Delete file + if ($row && $row['receipt_file'] && file_exists($row['receipt_file'])) unlink($row['receipt_file']); + + // Delete Journal Entry + if ($row && $row['journal_id']) { + delete_journal_entry($row['journal_id']); + } + + // Delete Expense $stmt = $db->prepare("DELETE FROM expenses WHERE id = ?"); $stmt->execute([$id]); + $_SESSION['success'] = 'تم حذف المصروف بنجاح'; redirect('expenses.php'); } diff --git a/hr_payroll.php b/hr_payroll.php index 4ef13cf..94a932d 100644 --- a/hr_payroll.php +++ b/hr_payroll.php @@ -1,5 +1,6 @@ ليس لديك صلاحية للوصول إلى هذه الصفحة.
"; @@ -14,6 +15,8 @@ $success = ''; // Handle Payroll Actions if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $action = $_POST['action'] ?? ''; + if (isset($_POST['generate_payroll'])) { if (!canAdd('hr_payroll')) { $error = "لا تملك صلاحية التوليد."; @@ -56,17 +59,59 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $status = $_POST['status']; // Recalculate Net - $stmt = db()->prepare("SELECT basic_salary FROM hr_payroll WHERE id = ?"); + $stmt = db()->prepare("SELECT * FROM hr_payroll WHERE id = ?"); $stmt->execute([$id]); $current = $stmt->fetch(); if ($current) { $net = $current['basic_salary'] + $bonuses - $deductions; $payment_date = ($status == 'paid') ? date('Y-m-d') : null; + $journal_id = $current['journal_id']; - $stmt = db()->prepare("UPDATE hr_payroll SET bonuses = ?, deductions = ?, net_salary = ?, status = ?, payment_date = ? WHERE id = ?"); - $stmt->execute([$bonuses, $deductions, $net, $status, $payment_date, $id]); - $success = "تم تحديث الراتب."; + try { + $db = db(); + // Update Payroll Record + $stmt = $db->prepare("UPDATE hr_payroll SET bonuses = ?, deductions = ?, net_salary = ?, status = ?, payment_date = ? WHERE id = ?"); + $stmt->execute([$bonuses, $deductions, $net, $status, $payment_date, $id]); + + // Accounting Integration + if ($status == 'paid') { + // Create or Update Journal Entry + $salary_account = 'مصروفات الرواتب'; // Account ID 13 + $cash_account = 'النقدية'; // Account ID 17 + + $entries = [ + ['account' => $salary_account, 'debit' => $net, 'credit' => 0], + ['account' => $cash_account, 'debit' => 0, 'credit' => $net] + ]; + + // Get Employee Name for Description + $stmt_emp = $db->prepare("SELECT first_name, last_name FROM hr_employees WHERE id = ?"); + $stmt_emp->execute([$current['employee_id']]); + $emp_name = $stmt_emp->fetch(PDO::FETCH_ASSOC); + $emp_full_name = $emp_name ? ($emp_name['first_name'] . ' ' . $emp_name['last_name']) : "Employee #{$current['employee_id']}"; + + $description = "Salary Payment: $emp_full_name ({$current['month']}/{$current['year']})"; + $reference = "PAY-{$current['year']}-{$current['month']}-{$current['employee_id']}"; + + if ($journal_id) { + edit_journal_entry($journal_id, $payment_date, $description, $reference, $entries); + } else { + $jid = add_journal_entry($payment_date, $description, $reference, $entries); + if ($jid) { + $db->prepare("UPDATE hr_payroll SET journal_id = ? WHERE id = ?")->execute([$jid, $id]); + } + } + } elseif ($status == 'pending' && $journal_id) { + // If changing back to pending, delete the journal entry + delete_journal_entry($journal_id); + $db->prepare("UPDATE hr_payroll SET journal_id = NULL WHERE id = ?")->execute([$id]); + } + + $success = "تم تحديث الراتب."; + } catch (Exception $e) { + $error = "حدث خطأ: " . $e->getMessage(); + } } } } @@ -164,6 +209,9 @@ foreach ($payrolls as $p) $total_salaries += $p['net_salary']; + + + @@ -260,4 +308,4 @@ foreach ($payrolls as $p) $total_salaries += $p['net_salary']; }); - + \ No newline at end of file diff --git a/includes/accounting_functions.php b/includes/accounting_functions.php index 478b10c..c6af7a3 100644 --- a/includes/accounting_functions.php +++ b/includes/accounting_functions.php @@ -90,7 +90,7 @@ function add_journal_entry($date, $description, $reference, $entries) { $stmt->execute([$journal_id, $entry['account'], $entry['debit'], $entry['credit']]); } $db->commit(); - return true; + return $journal_id; } catch (Exception $e) { $db->rollBack(); return false; diff --git a/includes/header.php b/includes/header.php index d4b0c1b..7adaa3e 100644 --- a/includes/header.php +++ b/includes/header.php @@ -118,7 +118,7 @@ if (!isLoggedIn() && basename($_SERVER['PHP_SELF']) !== 'login.php' && basename( // Determine active groups $cp = basename($_SERVER['PHP_SELF']); -$mail_pages = ['inbound.php', 'outbound.php', 'internal_inbox.php', 'internal_outbox.php']; +$mail_pages = ['inbound.php', 'outbound.php', 'internal_inbox.php', 'internal_outbox.php', 'overdue_report.php']; $is_mail_open = in_array($cp, $mail_pages); $acct_pages = ['accounting.php', 'trial_balance.php', 'balance_sheet.php', 'accounts.php']; @@ -133,9 +133,6 @@ $is_stock_open = in_array($cp, $stock_pages); $expenses_pages = ['expenses.php', 'expense_categories.php', 'expense_reports.php']; $is_expenses_open = in_array($cp, $expenses_pages); -$report_pages = ['overdue_report.php']; -$is_report_open = in_array($cp, $report_pages); - $admin_pages = ['index.php', 'users.php', 'charity-settings.php']; $is_admin_open = in_array($cp, $admin_pages); ?> @@ -276,7 +273,7 @@ $is_admin_open = in_array($cp, $admin_pages); - + + + + + @@ -521,27 +526,6 @@ $is_admin_open = in_array($cp, $admin_pages); - - - - -