Commit 5ed78ec8 authored by DevPilot's avatar DevPilot

feat(accounting): tools for the four things that still needed a human

Every item I had left as "the accountant must decide" now has a screen to decide
it on. Nothing accounting-related is left needing code.

**فئات الأصول — /inventory/asset-categories.** The screen I referenced in the
manual and in a seed comment, and had not written. Depreciation posts through
these three accounts per category; a category missing either depreciation
account is silently skipped, so the asset ages in the register and the balance
sheet never moves. The screen leads with exactly that failure — categories that
hold assets but cannot post — offers only postable accounts, and refuses a
half-mapped category, because one depreciation account without the other cannot
balance an entry.

**إعادة تبويب الحسابات — /accounting/reclassification.** A balance in the wrong
account cannot be edited: the ledger is the record, and a balance that disagrees
with the entries behind it is worse than a wrong one. So it moves by a dated,
balanced, reversible entry. It lists what is genuinely stuck — 8 accounts here,
including 43.9m on «مشروعات تحت التنفيذ» and 4.8m on a header — derives the
DIRECTION from the balance rather than asking (getting that backwards doubles a
balance instead of moving it), and refuses a header or cross-type destination.

To empty a header at all, JournalService needed to allow one posted there. That
is the only way out in double-entry, so the flag exists, is set by this service
alone, and the service independently refuses to let a header be the destination.

**رسملة مشروع تحت التنفيذ.** A finished project is not a purchase — the money
was spent over months and sits in CIP, which deliberately does not depreciate.
Capitalising moves the accumulated cost to the asset account so depreciation can
start. Booking it as a purchase would credit cash for money already spent. Now a
third option on the asset form, next to purchase and opening balance.

**أرشفة سنة مالية — /accounting/fiscal-years.** Overlapping years were flagged
but unfixable from the UI. Archiving retires one without deleting it, and is
refused outright when the year carries entries or is the current one.

The manual is rewritten around this: every wizard now has numbered UI steps, and
a new index lists all 39 accountant-facing tools with what each does. The
decisions section now names the tool for each item instead of describing the
problem.

Verified on a production clone with all 409 foreign keys: reclassification moves
4,799,436 off a header and leaves it at 0.00 with all guards firing,
capitalisation posts Dr Asset / Cr CIP and posts nothing without a CIP account,
categories screen offers 593 postable accounts and no headers, 1,499 routes
resolve, 254 controllers instantiate, 242 services load, trial balance diff 0.00,
and all 56 diagrams across docs/ parse against the real mermaid parser.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
parent c4a8f6cb
...@@ -126,6 +126,62 @@ class FiscalYearController extends Controller ...@@ -126,6 +126,62 @@ class FiscalYearController extends Controller
]); ]);
} }
/**
* Retire a fiscal year that should not be there.
*
* The club carries calendar years alongside a July–June year, so some days
* fall inside two open years at once — an entry in the overlap belongs to
* both, and closing one leaves the other open over the same transactions.
* Picking one convention means retiring the other.
*
* Archiving, not deleting: a fiscal year with entries against it is part of
* the record. Refused outright when entries exist, because removing it from
* date resolution would leave those entries pointing at a year the system
* no longer considers — they must be moved first.
*/
public function archive(Request $request, string $id): Response
{
$this->authorize('accounting.fiscal_year.manage');
$session = App::getInstance()->session();
$db = App::getInstance()->db();
$fy = FiscalYear::find((int) $id);
if (!$fy) {
$session->flash('_alerts', [['type' => 'error', 'message' => 'السنة المالية غير موجودة']]);
return $this->redirect('/accounting/fiscal-years');
}
$entries = (int) $db->selectOne(
"SELECT COUNT(*) AS c FROM journal_entries WHERE fiscal_year_id = ?",
[(int) $id]
)['c'];
if ($entries > 0) {
$session->flash('_alerts', [['type' => 'error', 'message' =>
'مينفعش تأرشف السنة دي — فيها ' . number_format($entries) . ' قيد. '
. 'القيود دي لازم تتنقل لسنة تانية الأول، وإلا هتفضل مربوطة بسنة النظام مش شايفها.']]);
return $this->redirect('/accounting/fiscal-years');
}
if ((int) $fy->is_current === 1) {
$session->flash('_alerts', [['type' => 'error', 'message' =>
'دي السنة الحالية — حدّد سنة تانية كحالية الأول.']]);
return $this->redirect('/accounting/fiscal-years');
}
$db->update('fiscal_years', [
'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
$session->flash('_alerts', [['type' => 'success', 'message' =>
'اتأرشفت السنة المالية. مش هتدخل في تحديد سنة أي قيد جديد.']]);
return $this->redirect('/accounting/fiscal-years');
}
public function close(Request $request, string $id): Response public function close(Request $request, string $id): Response
{ {
$this->authorize('accounting.fiscal_year.close'); $this->authorize('accounting.fiscal_year.close');
......
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\Accounting\Services\AccountReclassificationService;
/**
* إعادة تبويب الحسابات — moving a balance that ended up in the wrong account.
*
* The screen leads with the balances that are actually stuck: money sitting on
* a header account or on one that has been retired, where no ordinary entry can
* reach it. Nothing posts without a preview and a written reason.
*/
class ReclassificationController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('accounting.journal.view');
return $this->view('Accounting.Views.reclassification.index', [
'stranded' => AccountReclassificationService::strandedBalances(),
]);
}
/** What the correction would book. Answers without writing anything. */
public function preview(Request $request): Response
{
$this->authorize('accounting.journal.view');
return $this->json(AccountReclassificationService::preview(
(int) $request->get('from', 0),
(int) $request->get('to', 0),
$request->get('amount')
));
}
/** The destinations a given account's balance may legitimately move to. */
public function destinations(Request $request): Response
{
$this->authorize('accounting.journal.view');
return $this->json([
'accounts' => AccountReclassificationService::destinationsFor((int) $request->get('account', 0)),
]);
}
public function store(Request $request): Response
{
$this->authorize('accounting.journal.create');
$employee = $this->currentEmployee();
$result = AccountReclassificationService::post(
(int) $request->post('from_account_id', 0),
(int) $request->post('to_account_id', 0),
$request->post('amount'),
(string) $request->post('reason', ''),
$employee ? (int) $employee->id : null
);
if (empty($result['success'])) {
return $this->redirect('/accounting/reclassification')->withError($result['error']);
}
return $this->redirect('/accounting/reclassification')->withSuccess(
'اتعمل قيد إعادة التبويب' . (!empty($result['entry_number']) ? ' — ' . $result['entry_number'] : '')
. '. تقدر تعكسه من شاشة قيود اليومية لو حصل غلط.'
);
}
}
...@@ -11,6 +11,14 @@ return [ ...@@ -11,6 +11,14 @@ return [
['POST', '/accounting/fiscal-years', 'Accounting\Controllers\FiscalYearController@store', ['auth', 'csrf'], 'accounting.fiscal_year.manage'], ['POST', '/accounting/fiscal-years', 'Accounting\Controllers\FiscalYearController@store', ['auth', 'csrf'], 'accounting.fiscal_year.manage'],
['GET', '/accounting/fiscal-years/{id:\d+}', 'Accounting\Controllers\FiscalYearController@show', ['auth'], 'accounting.fiscal_year.view'], ['GET', '/accounting/fiscal-years/{id:\d+}', 'Accounting\Controllers\FiscalYearController@show', ['auth'], 'accounting.fiscal_year.view'],
['POST', '/accounting/fiscal-years/{id:\d+}/close', 'Accounting\Controllers\FiscalYearController@close', ['auth', 'csrf'], 'accounting.fiscal_year.close'], ['POST', '/accounting/fiscal-years/{id:\d+}/close', 'Accounting\Controllers\FiscalYearController@close', ['auth', 'csrf'], 'accounting.fiscal_year.close'],
// Resolving overlapping / superseded fiscal years without deleting history
['POST', '/accounting/fiscal-years/{id:\d+}/archive', 'Accounting\Controllers\FiscalYearController@archive', ['auth', 'csrf'], 'accounting.fiscal_year.manage'],
// إعادة تبويب الحسابات — moving a balance stuck on a header or retired account
['GET', '/accounting/reclassification', 'Accounting\Controllers\ReclassificationController@index', ['auth'], 'accounting.journal.view'],
['GET', '/accounting/reclassification/preview', 'Accounting\Controllers\ReclassificationController@preview', ['auth'], 'accounting.journal.view'],
['GET', '/accounting/reclassification/destinations', 'Accounting\Controllers\ReclassificationController@destinations', ['auth'], 'accounting.journal.view'],
['POST', '/accounting/reclassification', 'Accounting\Controllers\ReclassificationController@store', ['auth', 'csrf'], 'accounting.journal.create'],
// ── Chart of Accounts ──────────────────────────────────── // ── Chart of Accounts ────────────────────────────────────
['GET', '/accounting/chart-of-accounts', 'Accounting\Controllers\ChartOfAccountsController@index', ['auth'], 'accounting.coa.view'], ['GET', '/accounting/chart-of-accounts', 'Accounting\Controllers\ChartOfAccountsController@index', ['auth'], 'accounting.coa.view'],
......
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services;
use App\Core\App;
use App\Core\Logger;
/**
* Moving a balance from one account to another, with a real journal entry.
*
* Every ledger accumulates balances that ended up in the wrong place: an opening
* entry posted straight onto a header account, a cost booked to the parent
* instead of the child, a classification the auditor disagrees with. The
* balance cannot simply be edited — the ledger is the record, and a balance that
* disagrees with the entries behind it is worse than a wrong balance.
*
* So it is corrected the only honest way: a dated, balanced, reversible entry
* that debits one account and credits the other.
*
* Two rules this enforces that a hand-written entry would not:
*
* - the DIRECTION follows the account's nature, not a guess. Emptying a debit
* account means crediting it; emptying a credit account means debiting it.
* Getting this backwards doubles the balance instead of moving it.
* - the destination must be postable. Moving a balance from one header account
* onto another header account is the same mistake again.
*/
final class AccountReclassificationService
{
private const SCALE = 2;
/**
* Accounts carrying a balance that cannot be posted to any more.
*
* These are the ones that need this tool: money is sitting on a heading, so
* it cannot be drilled into, and no new entry can ever reach it to correct
* it by ordinary means.
*
* @return array<int, array<string, mixed>>
*/
public static function strandedBalances(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT a.id, a.account_code, a.name_ar, a.account_type, a.account_nature,
a.is_header, a.is_active, a.level,
ROUND(COALESCE(SUM(CASE WHEN e.status = 'posted' THEN l.debit ELSE 0 END), 0), 2) AS total_debit,
ROUND(COALESCE(SUM(CASE WHEN e.status = 'posted' THEN l.credit ELSE 0 END), 0), 2) AS total_credit,
COUNT(CASE WHEN e.status = 'posted' THEN l.id END) AS line_count,
(SELECT COUNT(*) FROM chart_of_accounts c
WHERE c.parent_id = a.id AND c.is_header = 0
AND c.is_active = 1 AND c.is_archived = 0) AS postable_children
FROM chart_of_accounts a
JOIN journal_entry_lines l ON l.account_id = a.id
JOIN journal_entries e ON e.id = l.journal_entry_id
WHERE a.is_archived = 0
AND (a.is_header = 1 OR a.is_active = 0)
GROUP BY a.id
HAVING ROUND(ABS(total_debit - total_credit), 2) > 0
-- Spelled out rather than reusing the aliases: `total_debit` also
-- names a column on journal_entries, and ORDER BY resolves to that
-- table column first, which trips only_full_group_by.
ORDER BY ABS(
COALESCE(SUM(CASE WHEN e.status = 'posted' THEN l.debit ELSE 0 END), 0)
- COALESCE(SUM(CASE WHEN e.status = 'posted' THEN l.credit ELSE 0 END), 0)
) DESC"
);
}
/** Where a stranded balance can legitimately go — the account's own children first. */
public static function destinationsFor(int $accountId): array
{
$db = App::getInstance()->db();
$children = $db->select(
"SELECT id, account_code, name_ar, account_type, 1 AS is_child
FROM chart_of_accounts
WHERE parent_id = ? AND is_header = 0 AND is_active = 1 AND is_archived = 0
ORDER BY account_code",
[$accountId]
);
$account = $db->selectOne(
"SELECT account_type FROM chart_of_accounts WHERE id = ?",
[$accountId]
);
if (!$account) {
return $children;
}
// Anything else of the same type. Moving a balance across types — an
// asset into a revenue account — is a different operation with different
// consequences, and is not what this tool is for.
$others = $db->select(
"SELECT id, account_code, name_ar, account_type, 0 AS is_child
FROM chart_of_accounts
WHERE account_type = ? AND is_header = 0 AND is_active = 1 AND is_archived = 0
AND (parent_id <> ? OR parent_id IS NULL)
ORDER BY account_code",
[$account['account_type'], $accountId]
);
return array_merge($children, $others);
}
/**
* What the correction would look like, without writing anything.
*
* @return array{ok:bool, error?:string, from?:array, to?:array, amount?:string, debit_account?:int, credit_account?:int}
*/
public static function preview(int $fromId, int $toId, ?string $amount = null): array
{
$db = App::getInstance()->db();
if ($fromId === $toId) {
return ['ok' => false, 'error' => 'الحساب المصدر والوجهة نفس الحساب'];
}
$from = $db->selectOne(
"SELECT id, account_code, name_ar, account_type, account_nature, is_header
FROM chart_of_accounts WHERE id = ? AND is_archived = 0",
[$fromId]
);
$to = $db->selectOne(
"SELECT id, account_code, name_ar, account_type, account_nature, is_header, is_active
FROM chart_of_accounts WHERE id = ? AND is_archived = 0",
[$toId]
);
if (!$from || !$to) {
return ['ok' => false, 'error' => 'حساب غير موجود'];
}
if ((int) $to['is_header'] === 1) {
return ['ok' => false, 'error' => 'حساب الوجهة رئيسي — مينفعش يتقيّد عليه. اختار حساب فرعي.'];
}
if ((int) $to['is_active'] === 0) {
return ['ok' => false, 'error' => 'حساب الوجهة موقوف'];
}
if ($from['account_type'] !== $to['account_type']) {
return ['ok' => false, 'error' => 'الحسابين من نوعين مختلفين — النقل ده بيغيّر تصنيف الميزانية وميتعملش من هنا'];
}
$bal = $db->selectOne(
"SELECT ROUND(COALESCE(SUM(l.debit), 0) - COALESCE(SUM(l.credit), 0), 2) AS net
FROM journal_entry_lines l
JOIN journal_entries e ON e.id = l.journal_entry_id
WHERE l.account_id = ? AND e.status = 'posted'",
[$fromId]
);
$net = (string) ($bal['net'] ?? '0.00');
if (bccomp($net, '0.00', self::SCALE) === 0) {
return ['ok' => false, 'error' => 'الحساب رصيده صفر — مفيش حاجة تتنقل'];
}
// A debit balance is emptied by crediting it, and the other way round.
// This is where a hand-written entry usually goes wrong.
$isDebitBalance = bccomp($net, '0.00', self::SCALE) > 0;
$full = $isDebitBalance ? $net : ltrim($net, '-');
$move = $amount !== null && $amount !== '' ? number_format((float) $amount, self::SCALE, '.', '') : $full;
if (bccomp($move, '0.00', self::SCALE) <= 0) {
return ['ok' => false, 'error' => 'المبلغ لازم يكون أكبر من صفر'];
}
if (bccomp($move, $full, self::SCALE) > 0) {
return ['ok' => false, 'error' => 'المبلغ أكبر من رصيد الحساب (' . $full . ')'];
}
return [
'ok' => true,
'from' => $from,
'to' => $to,
'current_net' => $net,
'amount' => $move,
'is_debit_balance' => $isDebitBalance,
// If the source carries a debit balance we credit it and debit the
// destination; otherwise the reverse.
'debit_account' => $isDebitBalance ? (int) $to['id'] : (int) $from['id'],
'credit_account' => $isDebitBalance ? (int) $from['id'] : (int) $to['id'],
];
}
/**
* Post the correction.
*
* @return array{success:bool, error?:string, entry_number?:string}
*/
public static function post(int $fromId, int $toId, ?string $amount, string $reason, ?int $employeeId = null): array
{
$p = self::preview($fromId, $toId, $amount);
if (empty($p['ok'])) {
return ['success' => false, 'error' => $p['error'] ?? 'تعذّر تجهيز القيد'];
}
$reason = trim($reason);
if ($reason === '') {
return ['success' => false, 'error' => 'لازم تكتب سبب إعادة التبويب — ده بيفضل في الدفاتر'];
}
$desc = 'إعادة تبويب — من ' . $p['from']['account_code'] . ' إلى ' . $p['to']['account_code'];
$result = JournalService::createEntry([
'entry_date' => date('Y-m-d'),
'description_ar' => $desc,
'description_en' => 'Account reclassification ' . $p['from']['account_code'] . ' → ' . $p['to']['account_code'],
'reference_type' => 'reclassification',
'source_module' => 'accounting',
'is_auto_generated' => 0,
'notes' => $reason,
// The whole point is to empty an account that can no longer be
// posted to. Without these the tool cannot do its only job. The
// destination is checked separately in preview() and can never be a
// header or an inactive account, so only the source side benefits.
'allow_inactive_accounts' => true,
'allow_header_accounts' => true,
], [
[
'account_id' => (int) $p['debit_account'],
'debit' => $p['amount'],
'credit' => '0.00',
'description_ar' => $desc,
],
[
'account_id' => (int) $p['credit_account'],
'debit' => '0.00',
'credit' => $p['amount'],
'description_ar' => $desc,
],
], true);
if (empty($result['success'])) {
Logger::error('Account reclassification failed', [
'from' => $fromId,
'to' => $toId,
'error' => $result['error'] ?? null,
]);
return ['success' => false, 'error' => $result['error'] ?? 'فشل ترحيل القيد'];
}
Logger::info('Account reclassified', [
'from' => $p['from']['account_code'],
'to' => $p['to']['account_code'],
'amount' => $p['amount'],
'by' => $employeeId,
]);
return ['success' => true, 'entry_number' => $result['entry_number'] ?? null];
}
}
...@@ -104,7 +104,15 @@ final class JournalService ...@@ -104,7 +104,15 @@ final class JournalService
if (!isset($accountMap[(int) $accId])) { if (!isset($accountMap[(int) $accId])) {
return ['success' => false, 'error' => 'الحساب رقم ' . $accId . ' غير موجود']; return ['success' => false, 'error' => 'الحساب رقم ' . $accId . ' غير موجود'];
} }
if ((int) $accountMap[(int) $accId]['is_header'] === 1) { // A header is a heading, not a place to post — with one exception.
// When a balance is ALREADY stranded on a header (a legacy opening
// entry, an import), double-entry offers no way to move it off
// except by posting the opposite side to that same header. Refusing
// here would make the error permanent. Only
// AccountReclassificationService sets this, and it independently
// refuses to let a header be the DESTINATION.
if ((int) $accountMap[(int) $accId]['is_header'] === 1
&& empty($header['allow_header_accounts'])) {
return ['success' => false, 'error' => 'لا يمكن الترحيل إلى حساب رئيسي (header)']; return ['success' => false, 'error' => 'لا يمكن الترحيل إلى حساب رئيسي (header)'];
} }
// An inactive account takes no new business — but the year-end entry // An inactive account takes no new business — but the year-end entry
......
...@@ -447,7 +447,8 @@ final class OperationalPostingService ...@@ -447,7 +447,8 @@ final class OperationalPostingService
// Already on the books — the register is catching up with the ledger, // Already on the books — the register is catching up with the ledger,
// not adding to it. // not adding to it.
if ((string) ($asset['acquisition_source'] ?? 'purchase') === 'opening') { $source = (string) ($asset['acquisition_source'] ?? 'purchase');
if ($source === 'opening') {
return; return;
} }
...@@ -465,12 +466,26 @@ final class OperationalPostingService ...@@ -465,12 +466,26 @@ final class OperationalPostingService
return; return;
} }
// How it was paid for decides only the credit side. // A finished project is not a purchase. The money was spent over months
$creditAccount = match ((string) ($data['payment_source'] ?? 'payable')) { // and already sits in «مشروعات تحت التنفيذ» — an asset that does not
'cash' => PostingRouter::accountFor('treasury:method_cash', AccountCodes::CASH_ON_HAND, 'collection'), // depreciate because it is not in service yet. Capitalising it moves the
'bank' => PostingRouter::accountFor('treasury:method_bank_transfer', AccountCodes::CASH_AT_BANK, 'collection'), // accumulated cost to the real asset account, from where it starts to
default => PostingRouter::accountFor('procurement:payable', AccountCodes::ACCOUNTS_PAYABLE, 'accrual'), // depreciate. Crediting cash instead would book the spend twice.
}; if ($source === 'capitalization') {
$cipAccount = (int) ($data['cip_account_id'] ?? 0);
if ($cipAccount <= 0) {
Logger::error('Capitalisation not posted — no CIP account given', ['asset_id' => $assetId]);
return;
}
$creditAccount = $cipAccount;
} else {
// How it was paid for decides only the credit side.
$creditAccount = match ((string) ($data['payment_source'] ?? 'payable')) {
'cash' => PostingRouter::accountFor('treasury:method_cash', AccountCodes::CASH_ON_HAND, 'collection'),
'bank' => PostingRouter::accountFor('treasury:method_bank_transfer', AccountCodes::CASH_AT_BANK, 'collection'),
default => PostingRouter::accountFor('procurement:payable', AccountCodes::ACCOUNTS_PAYABLE, 'accrual'),
};
}
if ($creditAccount === null) { if ($creditAccount === null) {
Logger::error('Asset acquisition not posted — funding account unresolved', [ Logger::error('Asset acquisition not posted — funding account unresolved', [
...@@ -481,7 +496,7 @@ final class OperationalPostingService ...@@ -481,7 +496,7 @@ final class OperationalPostingService
} }
$name = $asset['asset_tag'] ?: ('#' . $assetId); $name = $asset['asset_tag'] ?: ('#' . $assetId);
$desc = 'شراء أصل ثابت — ' . $name $desc = ($source === 'capitalization' ? 'رسملة مشروع تحت التنفيذ — ' : 'شراء أصل ثابت — ') . $name
. ($asset['category_name'] ? ' (' . $asset['category_name'] . ')' : ''); . ($asset['category_name'] ? ' (' . $asset['category_name'] . ')' : '');
$result = JournalService::createEntry([ $result = JournalService::createEntry([
...@@ -494,7 +509,8 @@ final class OperationalPostingService ...@@ -494,7 +509,8 @@ final class OperationalPostingService
'is_auto_generated' => 1, 'is_auto_generated' => 1,
], [ ], [
['account_id' => $assetAccount, 'debit' => $cost, 'credit' => '0.00', 'description_ar' => 'إثبات تكلفة الأصل — ' . $name], ['account_id' => $assetAccount, 'debit' => $cost, 'credit' => '0.00', 'description_ar' => 'إثبات تكلفة الأصل — ' . $name],
['account_id' => $creditAccount, 'debit' => '0.00', 'credit' => $cost, 'description_ar' => 'سداد/التزام شراء أصل — ' . $name], ['account_id' => $creditAccount, 'debit' => '0.00', 'credit' => $cost,
'description_ar' => ($source === 'capitalization' ? 'إقفال تكلفة المشروع — ' : 'سداد/التزام شراء أصل — ') . $name],
], true); ], true);
if (empty($result['success'])) { if (empty($result['success'])) {
......
...@@ -37,11 +37,25 @@ ...@@ -37,11 +37,25 @@
<td> <td>
<a href="/accounting/fiscal-years/<?= (int) $o['a_id'] ?>"><?= e((string) $o['a_name']) ?></a> <a href="/accounting/fiscal-years/<?= (int) $o['a_id'] ?>"><?= e((string) $o['a_name']) ?></a>
<span style="color:#9CA3AF;">(<?= e((string) $o['a_status']) ?>)</span> <span style="color:#9CA3AF;">(<?= e((string) $o['a_status']) ?>)</span>
<?php if (can('accounting.fiscal_year.manage')): ?>
<form method="POST" action="/accounting/fiscal-years/<?= (int) $o['a_id'] ?>/archive" style="display:inline;"
onsubmit="return confirm('هتأرشف «<?= e((string) $o['a_name']) ?>». مش هتدخل في تحديد سنة أي قيد جديد. متأكد؟');">
<?= csrf_field() ?>
<button type="submit" class="btn btn-sm btn-outline">أرشِف</button>
</form>
<?php endif; ?>
</td> </td>
<td style="direction:ltr;text-align:right;"><?= e((string) $o['a_start']) ?><?= e((string) $o['a_end']) ?></td> <td style="direction:ltr;text-align:right;"><?= e((string) $o['a_start']) ?><?= e((string) $o['a_end']) ?></td>
<td> <td>
<a href="/accounting/fiscal-years/<?= (int) $o['b_id'] ?>"><?= e((string) $o['b_name']) ?></a> <a href="/accounting/fiscal-years/<?= (int) $o['b_id'] ?>"><?= e((string) $o['b_name']) ?></a>
<span style="color:#9CA3AF;">(<?= e((string) $o['b_status']) ?>)</span> <span style="color:#9CA3AF;">(<?= e((string) $o['b_status']) ?>)</span>
<?php if (can('accounting.fiscal_year.manage')): ?>
<form method="POST" action="/accounting/fiscal-years/<?= (int) $o['b_id'] ?>/archive" style="display:inline;"
onsubmit="return confirm('هتأرشف «<?= e((string) $o['b_name']) ?>». مش هتدخل في تحديد سنة أي قيد جديد. متأكد؟');">
<?= csrf_field() ?>
<button type="submit" class="btn btn-sm btn-outline">أرشِف</button>
</form>
<?php endif; ?>
</td> </td>
<td style="direction:ltr;text-align:right;"><?= e((string) $o['b_start']) ?><?= e((string) $o['b_end']) ?></td> <td style="direction:ltr;text-align:right;"><?= e((string) $o['b_start']) ?><?= e((string) $o['b_end']) ?></td>
</tr> </tr>
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إعادة تبويب الحسابات<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="margin-bottom:16px;">
<h2 style="margin:6px 0 4px;">إعادة تبويب الحسابات</h2>
<p style="margin:0;color:#6B7280;font-size:13px;line-height:1.9;max-width:860px;">
لما رصيد يبقى في حساب غلط — قيد افتتاحي نزل على حساب رئيسي، أو تكلفة اتقيّدت
على الأب بدل الابن — الرصيد <strong>ما ينفعش يتعدّل بالإيد</strong>. الدفاتر هي
السجل، ورصيد مختلف عن القيود اللي وراه أسوأ من رصيد غلط.
<br><br>
الشاشة دي بتصلّحه بالطريقة الوحيدة الصح: <strong>قيد يومية مؤرّخ ومتوازن</strong>
يقفل الحساب الغلط ويفتح الصح. والقيد ده تقدر تعكسه زي أي قيد.
</p>
</div>
<?php if (empty($stranded)): ?>
<div class="card" style="border-right:3px solid #059669;">
<div style="padding:16px 18px;color:#065F46;font-size:13px;line-height:1.9;">
مفيش أرصدة عالقة. كل الأرصدة على حسابات فرعية نشطة تقدر تتقيّد عليها عادي.
</div>
</div>
<?php else: ?>
<div class="card" style="margin-bottom:15px;border-right:3px solid #DC2626;">
<div style="padding:14px 18px;">
<div style="font-size:15px;font-weight:700;margin-bottom:6px;">
أرصدة عالقة — <?= number_format(count($stranded)) ?>
</div>
<p style="margin:0;color:#991B1B;font-size:12.5px;line-height:1.9;">
الأرصدة دي على حسابات <strong>رئيسية</strong> أو <strong>موقوفة</strong> — يعني
مينفعش تعمل drill-down عليها، ومفيش قيد عادي يقدر يوصلها. لازم تتنقل لحساب فرعي نشط.
</p>
</div>
</div>
<div class="card">
<div class="table-responsive">
<table class="table" style="font-size:12.5px;">
<thead>
<tr>
<th>الحساب</th>
<th>النوع</th>
<th>ليه عالق</th>
<th>الرصيد</th>
<th>حسابات فرعية متاحة</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($stranded as $s): ?>
<?php
$net = bcsub((string) $s['total_debit'], (string) $s['total_credit'], 2);
$isDr = bccomp($net, '0.00', 2) > 0;
$shown = $isDr ? $net : ltrim($net, '-');
?>
<tr>
<td>
<code><?= e((string) $s['account_code']) ?></code>
<div style="font-weight:600;"><?= e((string) $s['name_ar']) ?></div>
</td>
<td><?= e((string) $s['account_type']) ?></td>
<td>
<?php if ((int) $s['is_header'] === 1): ?>
<span class="badge badge-danger">حساب رئيسي</span>
<?php else: ?>
<span class="badge badge-warning">موقوف</span>
<?php endif; ?>
</td>
<td style="font-weight:700;">
<?= money($shown) ?>
<span style="color:#6B7280;font-weight:400;"><?= $isDr ? 'مدين' : 'دائن' ?></span>
</td>
<td>
<?php if ((int) $s['postable_children'] > 0): ?>
<?= number_format((int) $s['postable_children']) ?>
<?php else: ?>
<span style="color:#B45309;">مفيش — هتختار حساب تاني</span>
<?php endif; ?>
</td>
<td>
<?php if (can('accounting.journal.create')): ?>
<button type="button" class="btn btn-sm btn-primary"
onclick="openMove(<?= (int) $s['id'] ?>, '<?= e(addslashes((string) $s['account_code'] . ' — ' . $s['name_ar'])) ?>', '<?= e($shown) ?>')">
انقل الرصيد
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (can('accounting.journal.create')): ?>
<div id="moveBox" class="card" style="margin-top:16px;display:none;border-right:3px solid #0D7377;">
<form method="POST" action="/accounting/reclassification">
<?= csrf_field() ?>
<input type="hidden" name="from_account_id" id="fromId">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;font-size:15px;">نقل رصيد — <span id="fromLabel"></span></h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">إلى حساب <span style="color:#DC2626;">*</span></label>
<select name="to_account_id" id="toId" class="form-input" required onchange="doPreview()">
<option value="">— جاري التحميل —</option>
</select>
<small style="color:#6B7280;">الحسابات الفرعية بتاعت نفس الحساب بتظهر الأول</small>
</div>
<div class="form-group">
<label class="form-label">المبلغ</label>
<input type="number" step="0.01" min="0.01" name="amount" id="amountIn" class="form-input"
style="direction:ltr;text-align:left;" oninput="doPreview()">
<small style="color:#6B7280;">سيبه فاضي عشان تنقل الرصيد كله</small>
</div>
</div>
<div class="form-group" style="margin-top:14px;">
<label class="form-label">سبب إعادة التبويب <span style="color:#DC2626;">*</span></label>
<textarea name="reason" class="form-input" rows="2" required
placeholder="مثال: رصيد افتتاحي نزل على الحساب الرئيسي بالغلط — بيتنقل للحساب الفرعي الصح"></textarea>
<small style="color:#6B7280;">بيتسجّل في القيد ويفضل في الدفاتر</small>
</div>
<div id="previewBox" style="margin-top:14px;padding:12px 14px;background:#F9FAFB;border-radius:6px;font-size:12.5px;line-height:1.9;color:#374151;">
اختار حساب الوجهة عشان تشوف القيد.
</div>
<div style="display:flex;gap:10px;margin-top:16px;">
<button type="submit" class="btn btn-primary" id="submitBtn" disabled>رحّل القيد</button>
<button type="button" class="btn btn-outline" onclick="document.getElementById('moveBox').style.display='none';">إلغاء</button>
</div>
</div>
</form>
</div>
<script>
function openMove(id, label, amount) {
document.getElementById('fromId').value = id;
document.getElementById('fromLabel').textContent = label;
document.getElementById('amountIn').placeholder = 'الرصيد كله: ' + amount;
document.getElementById('amountIn').value = '';
document.getElementById('moveBox').style.display = '';
document.getElementById('submitBtn').disabled = true;
document.getElementById('previewBox').textContent = 'اختار حساب الوجهة عشان تشوف القيد.';
fetch('/accounting/reclassification/destinations?account=' + id)
.then(function (r) { return r.json(); })
.then(function (d) {
var sel = document.getElementById('toId');
sel.innerHTML = '<option value="">— اختار الحساب —</option>';
var group = null;
(d.accounts || []).forEach(function (a) {
var want = Number(a.is_child) === 1 ? 'حسابات فرعية من نفس الحساب' : 'حسابات تانية من نفس النوع';
if (group === null || group.label !== want) {
group = document.createElement('optgroup');
group.label = want;
sel.appendChild(group);
}
var o = document.createElement('option');
o.value = a.id;
o.textContent = a.account_code + ' — ' + a.name_ar;
group.appendChild(o);
});
document.getElementById('moveBox').scrollIntoView({ behavior: 'smooth', block: 'center' });
});
}
function doPreview() {
var from = document.getElementById('fromId').value;
var to = document.getElementById('toId').value;
var amt = document.getElementById('amountIn').value;
var box = document.getElementById('previewBox');
var btn = document.getElementById('submitBtn');
if (!from || !to) { btn.disabled = true; return; }
fetch('/accounting/reclassification/preview?from=' + from + '&to=' + to + '&amount=' + encodeURIComponent(amt))
.then(function (r) { return r.json(); })
.then(function (p) {
if (!p.ok) {
box.innerHTML = '<span style="color:#991B1B;">' + (p.error || 'مش هينفع') + '</span>';
btn.disabled = true;
return;
}
var drIsTo = Number(p.debit_account) === Number(to);
box.innerHTML =
'<strong>القيد اللي هيتعمل:</strong><br>' +
'<span style="direction:rtl;display:block;margin-top:6px;">' +
'من ح/ ' + (drIsTo ? p.to.account_code + ' — ' + p.to.name_ar : p.from.account_code + ' — ' + p.from.name_ar) +
' &nbsp;<strong>' + p.amount + '</strong><br>' +
'&nbsp;&nbsp;&nbsp;&nbsp;إلى ح/ ' + (drIsTo ? p.from.account_code + ' — ' + p.from.name_ar : p.to.account_code + ' — ' + p.to.name_ar) +
' &nbsp;<strong>' + p.amount + '</strong>' +
'</span>';
btn.disabled = false;
});
}
</script>
<?php endif; ?>
<?php endif; ?>
<?php $__template->endSection(); ?>
...@@ -176,6 +176,7 @@ MenuRegistry::register('accounting', [ ...@@ -176,6 +176,7 @@ MenuRegistry::register('accounting', [
['label_ar' => 'الأبعاد المحاسبية', 'label_en' => 'Dimensions', 'route' => '/accounting/dimensions', 'permission' => 'accounting.dimensions.view', 'order' => 9], ['label_ar' => 'الأبعاد المحاسبية', 'label_en' => 'Dimensions', 'route' => '/accounting/dimensions', 'permission' => 'accounting.dimensions.view', 'order' => 9],
['label_ar' => 'المطابقة البنكية', 'label_en' => 'Bank Reconciliation', 'route' => '/accounting/bank-reconciliation', 'permission' => 'accounting.bank_recon.view', 'order' => 10], ['label_ar' => 'المطابقة البنكية', 'label_en' => 'Bank Reconciliation', 'route' => '/accounting/bank-reconciliation', 'permission' => 'accounting.bank_recon.view', 'order' => 10],
['label_ar' => 'الحركات اليومية', 'label_en' => 'Daily Transactions', 'route' => '/accounting/daily-transactions', 'permission' => 'accounting.daily_tx.view', 'order' => 11], ['label_ar' => 'الحركات اليومية', 'label_en' => 'Daily Transactions', 'route' => '/accounting/daily-transactions', 'permission' => 'accounting.daily_tx.view', 'order' => 11],
['label_ar' => 'إعادة تبويب الحسابات', 'label_en' => 'Reclassification', 'route' => '/accounting/reclassification', 'permission' => 'accounting.journal.view', 'order' => 26],
['label_ar' => 'القيود الافتتاحية', 'label_en' => 'Opening Entries', 'route' => '/accounting/opening-entries', 'permission' => 'accounting.opening_entry.view', 'order' => 12], ['label_ar' => 'القيود الافتتاحية', 'label_en' => 'Opening Entries', 'route' => '/accounting/opening-entries', 'permission' => 'accounting.opening_entry.view', 'order' => 12],
['label_ar' => 'إقفال الفترات', 'label_en' => 'Period Closing', 'route' => '/accounting/period-closing', 'permission' => 'accounting.period.view', 'order' => 12], ['label_ar' => 'إقفال الفترات', 'label_en' => 'Period Closing', 'route' => '/accounting/period-closing', 'permission' => 'accounting.period.view', 'order' => 12],
['label_ar' => 'ميزان المراجعة', 'label_en' => 'Trial Balance', 'route' => '/accounting/reports/trial-balance', 'permission' => 'accounting.reports.trial_balance', 'order' => 9], ['label_ar' => 'ميزان المراجعة', 'label_en' => 'Trial Balance', 'route' => '/accounting/reports/trial-balance', 'permission' => 'accounting.reports.trial_balance', 'order' => 9],
......
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Controllers;
use App\Core\App;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
/**
* Fixed-asset categories, and the three ledger accounts each one carries.
*
* This screen is the whole reason depreciation reaches the books. The monthly
* run groups by category and posts, per category:
*
* Dr مصروف الإهلاك (expense_account_id)
* Cr مجمع الإهلاك (depreciation_account_id)
*
* A category with either account missing is skipped and logged — the asset ages
* in the register and the balance sheet never moves. That failure is silent by
* design elsewhere, so it is made loud here instead.
*/
class AssetCategoryController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('asset.view');
return $this->view('Inventory.Views.asset_categories.index', [
'categories' => $this->listWithAccounts(),
]);
}
public function create(Request $request): Response
{
$this->authorize('asset.manage');
return $this->view('Inventory.Views.asset_categories.form', [
'category' => null,
'accounts' => $this->postableAccounts(),
'methods' => ['straight_line' => 'القسط الثابت', 'declining_balance' => 'القسط المتناقص'],
]);
}
public function edit(Request $request, string $id): Response
{
$this->authorize('asset.manage');
$category = App::getInstance()->db()->selectOne(
"SELECT * FROM asset_categories WHERE id = ?",
[(int) $id]
);
if (!$category) {
return $this->redirect('/inventory/asset-categories')->withError('الفئة غير موجودة');
}
return $this->view('Inventory.Views.asset_categories.form', [
'category' => $category,
'accounts' => $this->postableAccounts(),
'methods' => ['straight_line' => 'القسط الثابت', 'declining_balance' => 'القسط المتناقص'],
]);
}
public function store(Request $request): Response
{
$this->authorize('asset.manage');
$data = $this->categoryInput($request);
if (isset($data['error'])) {
return $this->redirect('/inventory/asset-categories/create')->withError($data['error']);
}
$db = App::getInstance()->db();
if ($db->selectOne("SELECT id FROM asset_categories WHERE name_ar = ?", [$data['name_ar']])) {
return $this->redirect('/inventory/asset-categories/create')->withError('فيه فئة بنفس الاسم');
}
$db->insert('asset_categories', $data + ['is_active' => 1, 'created_at' => date('Y-m-d H:i:s')]);
return $this->redirect('/inventory/asset-categories')->withSuccess('اتضافت الفئة.');
}
public function update(Request $request, string $id): Response
{
$this->authorize('asset.manage');
$db = App::getInstance()->db();
if (!$db->selectOne("SELECT id FROM asset_categories WHERE id = ?", [(int) $id])) {
return $this->redirect('/inventory/asset-categories')->withError('الفئة غير موجودة');
}
$data = $this->categoryInput($request);
if (isset($data['error'])) {
return $this->redirect('/inventory/asset-categories/' . $id . '/edit')->withError($data['error']);
}
$data['is_active'] = (int) $request->post('is_active', 1) === 1 ? 1 : 0;
$db->update('asset_categories', $data, '`id` = ?', [(int) $id]);
// Changing a category re-points every FUTURE depreciation posting for
// its assets. Entries already posted are history and stay where they
// are — moving them would rewrite closed periods.
return $this->redirect('/inventory/asset-categories')->withSuccess(
'اتحدّثت الفئة. الإهلاك الجاي هيتقيّد على الحسابات الجديدة — القيود القديمة زي ما هي.'
);
}
/** Categories with their accounts resolved, and how many assets ride on each. */
private function listWithAccounts(): array
{
return App::getInstance()->db()->select(
"SELECT c.*,
a.account_code AS asset_code, a.name_ar AS asset_name,
d.account_code AS accum_code, d.name_ar AS accum_name,
e.account_code AS expense_code, e.name_ar AS expense_name,
(SELECT COUNT(*) FROM asset_register r
WHERE r.category_id = c.id AND r.status = 'active') AS asset_count,
(SELECT ROUND(COALESCE(SUM(r.book_value), 0), 2) FROM asset_register r
WHERE r.category_id = c.id AND r.status = 'active') AS book_value
FROM asset_categories c
LEFT JOIN chart_of_accounts a ON a.id = c.asset_account_id
LEFT JOIN chart_of_accounts d ON d.id = c.depreciation_account_id
LEFT JOIN chart_of_accounts e ON e.id = c.expense_account_id
ORDER BY c.is_active DESC, c.id"
);
}
/**
* Only accounts an entry can actually land on.
*
* A header account is a heading, not a place to post — offering one here
* would produce a category that fails at the moment depreciation runs,
* which is the worst possible time to find out.
*/
private function postableAccounts(): array
{
return App::getInstance()->db()->select(
"SELECT id, account_code, name_ar, account_type
FROM chart_of_accounts
WHERE is_header = 0 AND is_active = 1 AND is_archived = 0
ORDER BY account_code"
);
}
/** @return array<string, mixed> with an `error` key when unusable */
private function categoryInput(Request $request): array
{
$name = trim((string) $request->post('name_ar', ''));
if ($name === '') {
return ['error' => 'اسم الفئة مطلوب'];
}
$assetAccount = (int) $request->post('asset_account_id', 0);
if ($assetAccount <= 0) {
return ['error' => 'حساب الأصل مطلوب — من غيره الأصل مش هيتقيّد'];
}
$accum = (int) $request->post('depreciation_account_id', 0);
$expense = (int) $request->post('expense_account_id', 0);
$life = (int) $request->post('default_useful_life_months', 60);
// Either both depreciation accounts or neither. One alone cannot post a
// balanced entry, and a category that silently skips is how the balance
// sheet stops matching the register.
if (($accum > 0) !== ($expense > 0)) {
return ['error' => 'حسابات الإهلاك لازم يتحطوا مع بعض — مجمع الإهلاك ومصروف الإهلاك، أو تسيبهم الاتنين فاضيين لأصل ما بيتهلكش'];
}
if ($accum > 0 && $life < 1) {
return ['error' => 'العمر الإنتاجي لازم يكون شهر على الأقل لفئة بتتهلك'];
}
$method = (string) $request->post('depreciation_method', 'straight_line');
$rate = $request->post('declining_rate');
if ($method === 'declining_balance' && (!is_numeric($rate) || (float) $rate <= 0)) {
return ['error' => 'نسبة القسط المتناقص مطلوبة ولازم تكون أكبر من صفر'];
}
// Every account must exist and be postable, whatever the form sent.
$db = App::getInstance()->db();
foreach (['حساب الأصل' => $assetAccount, 'مجمع الإهلاك' => $accum, 'مصروف الإهلاك' => $expense] as $label => $accId) {
if ($accId <= 0) {
continue;
}
$acc = $db->selectOne(
"SELECT is_header, is_active FROM chart_of_accounts WHERE id = ? AND is_archived = 0",
[$accId]
);
if (!$acc) {
return ['error' => $label . ' — الحساب غير موجود'];
}
if ((int) $acc['is_header'] === 1) {
return ['error' => $label . ' — مينفعش حساب رئيسي، اختار حساب فرعي'];
}
if ((int) $acc['is_active'] === 0) {
return ['error' => $label . ' — الحساب موقوف'];
}
}
return [
'name_ar' => $name,
'name_en' => trim((string) $request->post('name_en', '')) ?: null,
'depreciation_method' => \in_array($method, ['straight_line', 'declining_balance'], true) ? $method : 'straight_line',
'default_useful_life_months' => max(1, $life),
'default_salvage_percentage' => number_format((float) $request->post('default_salvage_percentage', 0), 2, '.', ''),
'declining_rate' => $method === 'declining_balance' ? number_format((float) $rate, 2, '.', '') : null,
'asset_account_id' => $assetAccount,
'depreciation_account_id' => $accum > 0 ? $accum : null,
'expense_account_id' => $expense > 0 ? $expense : null,
];
}
}
...@@ -129,6 +129,7 @@ class AssetController extends Controller ...@@ -129,6 +129,7 @@ class AssetController extends Controller
'warehouses' => Warehouse::allActive(), 'warehouses' => Warehouse::allActive(),
'methods' => AssetRegister::getDepreciationMethods(), 'methods' => AssetRegister::getDepreciationMethods(),
'nextTag' => $this->nextAssetTag(), 'nextTag' => $this->nextAssetTag(),
'cipAccounts' => $this->cipAccounts(),
]); ]);
} }
...@@ -180,12 +181,15 @@ class AssetController extends Controller ...@@ -180,12 +181,15 @@ class AssetController extends Controller
EventBus::dispatch('inventory.asset_acquired', [ EventBus::dispatch('inventory.asset_acquired', [
'asset_id' => $assetId, 'asset_id' => $assetId,
'payment_source' => $data['payment_source'], 'payment_source' => $data['payment_source'],
'cip_account_id' => $data['cip_account_id'],
]); ]);
return $this->redirect('/inventory/assets/' . $assetId)->withSuccess( return $this->redirect('/inventory/assets/' . $assetId)->withSuccess(
$data['acquisition_source'] === 'opening' match ($data['acquisition_source']) {
? 'اتسجّل الأصل في السجل. رصيد افتتاحي — مفيش قيد اتعمل لأن التكلفة موجودة في الدفاتر أصلًا.' 'opening' => 'اتسجّل الأصل في السجل. رصيد افتتاحي — مفيش قيد اتعمل لأن التكلفة موجودة في الدفاتر أصلًا.',
: 'اتسجّل الأصل واتعمل قيد الشراء.' 'capitalization' => 'اتسجّل الأصل واتعمل قيد الرسملة — التكلفة اتنقلت من المشروعات تحت التنفيذ لحساب الأصل، ويبدأ الإهلاك من دلوقتي.',
default => 'اتسجّل الأصل واتعمل قيد الشراء.',
}
); );
} }
...@@ -207,6 +211,7 @@ class AssetController extends Controller ...@@ -207,6 +211,7 @@ class AssetController extends Controller
'warehouses' => Warehouse::allActive(), 'warehouses' => Warehouse::allActive(),
'methods' => AssetRegister::getDepreciationMethods(), 'methods' => AssetRegister::getDepreciationMethods(),
'nextTag' => $asset['asset_tag'], 'nextTag' => $asset['asset_tag'],
'cipAccounts' => $this->cipAccounts(),
]); ]);
} }
...@@ -280,6 +285,26 @@ class AssetController extends Controller ...@@ -280,6 +285,26 @@ class AssetController extends Controller
); );
} }
/**
* Accounts a finished project can be capitalised OUT of.
*
* «مشروعات تحت التنفيذ» is an asset that deliberately does not depreciate —
* the thing is not in service yet. When it is finished the accumulated cost
* moves to the real asset account and depreciation starts. Anything under
* 1103, plus any postable asset account whose name says so.
*/
private function cipAccounts(): array
{
return App::getInstance()->db()->select(
"SELECT id, account_code, name_ar, current_balance
FROM chart_of_accounts
WHERE is_header = 0 AND is_active = 1 AND is_archived = 0
AND account_type = 'asset'
AND (account_code LIKE '1103%' OR name_ar LIKE '%تحت التنفيذ%')
ORDER BY account_code"
);
}
private function nextAssetTag(): string private function nextAssetTag(): string
{ {
$row = App::getInstance()->db()->selectOne( $row = App::getInstance()->db()->selectOne(
...@@ -337,6 +362,10 @@ class AssetController extends Controller ...@@ -337,6 +362,10 @@ class AssetController extends Controller
$source = (string) $request->post('acquisition_source', 'purchase'); $source = (string) $request->post('acquisition_source', 'purchase');
if ($source === 'capitalization' && (int) $request->post('cip_account_id', 0) <= 0) {
return ['error' => 'لازم تحدد حساب المشروع تحت التنفيذ اللي التكلفة هتتنقل منه'];
}
return [ return [
'asset_tag' => $tag, 'asset_tag' => $tag,
'category_id' => $categoryId, 'category_id' => $categoryId,
...@@ -345,8 +374,9 @@ class AssetController extends Controller ...@@ -345,8 +374,9 @@ class AssetController extends Controller
'serial_number' => trim((string) $request->post('serial_number', '')) ?: null, 'serial_number' => trim((string) $request->post('serial_number', '')) ?: null,
'purchase_date' => $date, 'purchase_date' => $date,
'purchase_cost' => number_format((float) $cost, 2, '.', ''), 'purchase_cost' => number_format((float) $cost, 2, '.', ''),
'acquisition_source' => \in_array($source, ['purchase', 'opening'], true) ? $source : 'purchase', 'acquisition_source' => \in_array($source, ['purchase', 'opening', 'capitalization'], true) ? $source : 'purchase',
'payment_source' => (string) $request->post('payment_source', 'payable'), 'payment_source' => (string) $request->post('payment_source', 'payable'),
'cip_account_id' => ((int) $request->post('cip_account_id', 0)) ?: null,
'useful_life_months' => max(1, (int) $request->post('useful_life_months', 60)), 'useful_life_months' => max(1, (int) $request->post('useful_life_months', 60)),
'salvage_value' => number_format((float) $request->post('salvage_value', 0), 2, '.', ''), 'salvage_value' => number_format((float) $request->post('salvage_value', 0), 2, '.', ''),
'depreciation_method' => (string) $request->post('depreciation_method', 'straight_line'), 'depreciation_method' => (string) $request->post('depreciation_method', 'straight_line'),
......
...@@ -77,6 +77,13 @@ return [ ...@@ -77,6 +77,13 @@ return [
['POST', '/inventory/assets/{id:\d+}/dispose', 'Inventory\Controllers\AssetController@dispose', ['auth', 'csrf'], 'asset.manage'], ['POST', '/inventory/assets/{id:\d+}/dispose', 'Inventory\Controllers\AssetController@dispose', ['auth', 'csrf'], 'asset.manage'],
['POST', '/inventory/assets/run-depreciation', 'Inventory\Controllers\AssetController@runDepreciation',['auth', 'csrf'], 'asset.manage'], ['POST', '/inventory/assets/run-depreciation', 'Inventory\Controllers\AssetController@runDepreciation',['auth', 'csrf'], 'asset.manage'],
// Fixed-asset categories — the GL mapping depreciation posts through
['GET', '/inventory/asset-categories', 'Inventory\Controllers\AssetCategoryController@index', ['auth'], 'asset.view'],
['GET', '/inventory/asset-categories/create', 'Inventory\Controllers\AssetCategoryController@create', ['auth'], 'asset.manage'],
['POST', '/inventory/asset-categories', 'Inventory\Controllers\AssetCategoryController@store', ['auth', 'csrf'], 'asset.manage'],
['GET', '/inventory/asset-categories/{id:\d+}/edit', 'Inventory\Controllers\AssetCategoryController@edit', ['auth'], 'asset.manage'],
['POST', '/inventory/asset-categories/{id:\d+}', 'Inventory\Controllers\AssetCategoryController@update', ['auth', 'csrf'], 'asset.manage'],
// Bill of Materials (BOM) // Bill of Materials (BOM)
['GET', '/inventory/bom', 'Inventory\Controllers\BomController@index', ['auth'], 'inventory.bom.view'], ['GET', '/inventory/bom', 'Inventory\Controllers\BomController@index', ['auth'], 'inventory.bom.view'],
['GET', '/inventory/bom/create', 'Inventory\Controllers\BomController@create', ['auth'], 'inventory.bom.manage'], ['GET', '/inventory/bom/create', 'Inventory\Controllers\BomController@create', ['auth'], 'inventory.bom.manage'],
......
<?php
$isEdit = $category !== null;
$__template->layout('Layout.main');
/** Render one account picker, grouped so the right account is findable. */
$accountSelect = static function (string $name, $selected, array $accounts, bool $required = false): void {
echo '<select name="' . e($name) . '" class="form-input"' . ($required ? ' required' : '') . '>';
echo '<option value="">— مش محدد —</option>';
$groups = ['asset' => 'أصول', 'liability' => 'التزامات', 'equity' => 'حقوق ملكية', 'revenue' => 'إيرادات', 'expense' => 'مصروفات'];
foreach ($groups as $type => $label) {
$rows = array_filter($accounts, static fn ($a) => (string) $a['account_type'] === $type);
if (!$rows) {
continue;
}
echo '<optgroup label="' . e($label) . '">';
foreach ($rows as $a) {
$sel = (int) $selected === (int) $a['id'] ? ' selected' : '';
echo '<option value="' . (int) $a['id'] . '"' . $sel . '>'
. e((string) $a['account_code']) . ' — ' . e((string) $a['name_ar'])
. '</option>';
}
echo '</optgroup>';
}
echo '</select>';
};
?>
<?php $__template->section('title'); ?><?= $isEdit ? 'تعديل فئة أصول' : 'فئة أصول جديدة' ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/asset-categories" class="btn btn-outline">العودة للفئات</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="<?= $isEdit ? '/inventory/asset-categories/' . (int) $category['id'] : '/inventory/asset-categories' ?>">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات الفئة</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">اسم الفئة <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" class="form-input" required
value="<?= e(old('name_ar', (string) ($category['name_ar'] ?? ''))) ?>">
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" class="form-input" style="direction:ltr;text-align:left;"
value="<?= e(old('name_en', (string) ($category['name_en'] ?? ''))) ?>">
</div>
</div>
<?php if ($isEdit): ?>
<div class="form-group" style="margin-top:12px;">
<label style="font-size:13px;">
<input type="checkbox" name="is_active" value="1" <?= (int) ($category['is_active'] ?? 1) === 1 ? 'checked' : '' ?>>
الفئة نشطة
</label>
</div>
<?php endif; ?>
</div>
</div>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;font-size:15px;">حسابات الأستاذ</h3>
</div>
<div style="padding:20px;">
<div style="margin-bottom:16px;padding:12px 14px;background:#F0F9FF;border-radius:6px;color:#075985;font-size:12.5px;line-height:1.9;">
القيد الشهري للفئة دي هيبقى:
<br>
<span style="direction:ltr;display:inline-block;margin-top:4px;">
<strong>Dr</strong> مصروف الإهلاك &nbsp;&nbsp; <strong>Cr</strong> مجمع الإهلاك
</span>
<br>
<strong>القوائم بتعرض بس الحسابات الفرعية النشطة</strong> — الحساب الرئيسي
مينفعش يتقيّد عليه.
</div>
<div style="display:grid;grid-template-columns:1fr;gap:16px;">
<div class="form-group">
<label class="form-label">حساب الأصل <span style="color:#DC2626;">*</span></label>
<?php $accountSelect('asset_account_id', $category['asset_account_id'] ?? 0, $accounts, true); ?>
<small style="color:#6B7280;">الحساب اللي تكلفة الأصل بتتقيّد عليه — مثال: ١١٠١٠٣٠١ آلات</small>
</div>
<div class="form-group">
<label class="form-label">مجمع الإهلاك</label>
<?php $accountSelect('depreciation_account_id', $category['depreciation_account_id'] ?? 0, $accounts); ?>
<small style="color:#6B7280;">حساب مقابل للأصل — مثال: ٢٣٠١٠١٠٢</small>
</div>
<div class="form-group">
<label class="form-label">مصروف الإهلاك</label>
<?php $accountSelect('expense_account_id', $category['expense_account_id'] ?? 0, $accounts); ?>
<small style="color:#6B7280;">
بيتبع وظيفة الأصل: <strong>٣١٣٧xx</strong> للنشاط،
<strong>٣٢٠٦xx</strong> للبيع والتوزيع،
<strong>٣٣١٦xx</strong> للعمومية والإدارية
</small>
</div>
</div>
<div style="margin-top:14px;padding:12px 14px;background:#FFFBEB;border-radius:6px;color:#92400E;font-size:12.5px;line-height:1.9;">
<strong>الاتنين مع بعض أو ولا واحد.</strong> لو حطيت مجمع الإهلاك من غير
مصروف الإهلاك (أو العكس) القيد مش هيتوازن والفئة هتتخطّى بالكامل.
سيبهم الاتنين فاضيين بس لو الفئة دي <strong>ما بتتهلكش</strong> — زي الأراضي.
</div>
</div>
</div>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;font-size:15px;">سياسة الإهلاك الافتراضية</h3>
</div>
<div style="padding:20px;">
<p style="margin:0 0 14px;color:#6B7280;font-size:12px;line-height:1.9;">
دي <strong>قيم افتراضية</strong> بتتملّى لوحدها لما تسجّل أصل جديد في الفئة دي.
كل أصل بيحتفظ بعمره هو — تغيير الفئة دلوقتي ما بيغيّرش الأصول المسجّلة قبل كده.
</p>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">طريقة الإهلاك</label>
<select name="depreciation_method" id="methodSel" class="form-input"
onchange="document.getElementById('rateBox').style.display = this.value === 'declining_balance' ? '' : 'none';">
<?php foreach ($methods as $k => $label): ?>
<option value="<?= e($k) ?>" <?= old('depreciation_method', (string) ($category['depreciation_method'] ?? 'straight_line')) === $k ? 'selected' : '' ?>>
<?= e($label) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">العمر الإنتاجي (شهور)</label>
<input type="number" min="1" name="default_useful_life_months" class="form-input" style="direction:ltr;text-align:left;"
value="<?= e(old('default_useful_life_months', (string) ($category['default_useful_life_months'] ?? 60))) ?>">
</div>
<div class="form-group">
<label class="form-label">القيمة التخريدية %</label>
<input type="number" step="0.01" min="0" max="100" name="default_salvage_percentage" class="form-input" style="direction:ltr;text-align:left;"
value="<?= e(old('default_salvage_percentage', (string) ($category['default_salvage_percentage'] ?? '0.00'))) ?>">
</div>
<div class="form-group" id="rateBox" style="display:<?= old('depreciation_method', (string) ($category['depreciation_method'] ?? 'straight_line')) === 'declining_balance' ? '' : 'none' ?>;">
<label class="form-label">نسبة القسط المتناقص %</label>
<input type="number" step="0.01" min="0" name="declining_rate" class="form-input" style="direction:ltr;text-align:left;"
value="<?= e(old('declining_rate', (string) ($category['declining_rate'] ?? ''))) ?>">
</div>
</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary"><?= $isEdit ? 'حفظ التعديلات' : 'إضافة الفئة' ?></button>
<a href="/inventory/asset-categories" class="btn btn-outline">إلغاء</a>
</div>
</form>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>فئات الأصول الثابتة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if (can('asset.manage')): ?>
<a href="/inventory/asset-categories/create" class="btn btn-primary">+ فئة جديدة</a>
<?php endif; ?>
<a href="/inventory/assets" class="btn btn-outline">سجل الأصول</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$broken = [];
foreach ($categories as $c) {
if ((int) $c['is_active'] !== 1) {
continue;
}
// A category with no depreciation accounts is only correct when nothing in
// it depreciates — land. Anything else with assets on it is a hole.
$noDep = empty($c['accum_code']) || empty($c['expense_code']);
if ($noDep && (int) $c['asset_count'] > 0) {
$broken[] = $c;
}
}
?>
<?php if ($broken): ?>
<div class="card" style="margin-bottom:15px;border-right:3px solid #DC2626;">
<div style="padding:14px 18px;">
<div style="font-size:15px;font-weight:700;margin-bottom:6px;">
فئات فيها أصول ومن غير حسابات إهلاك — <?= number_format(count($broken)) ?>
</div>
<p style="margin:0;color:#991B1B;font-size:12.5px;line-height:1.9;">
الأصول اللي في الفئات دي <strong>مش بيتقيّد إهلاكها</strong>. الإهلاك بيتحسب في
السجل، وأول ما يوصل للترحيل بيتخطّى الفئة عشان الحساب ناقص — يعني الأرباح
بتطلع أعلى من الحقيقة والأصول في الميزانية أعلى من الحقيقة.
<br>
حدّد <strong>مجمع الإهلاك</strong> و<strong>مصروف الإهلاك</strong> للفئات دي.
</p>
<ul style="margin:10px 0 0;padding-right:18px;color:#374151;font-size:12.5px;line-height:2;">
<?php foreach ($broken as $c): ?>
<li>
<a href="/inventory/asset-categories/<?= (int) $c['id'] ?>/edit"><?= e((string) $c['name_ar']) ?></a>
<?= number_format((int) $c['asset_count']) ?> أصل بقيمة <?= money((string) $c['book_value']) ?>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<?php endif; ?>
<div class="card" style="margin-bottom:15px;border-right:3px solid #0D7377;">
<div style="padding:14px 18px;color:#374151;font-size:12.5px;line-height:1.9;">
الفئة هي اللي بتوصّل الأصل بالدفاتر. كل فئة شايلة <strong>٣ حسابات</strong>:
حساب الأصل نفسه، ومجمع الإهلاك، ومصروف الإهلاك. قيد الإهلاك الشهري بيتجمّع
<strong>بالفئة</strong> مش بالأصل — يعني قيد واحد فيه سطرين لكل فئة.
<br>
<strong>مصروف الإهلاك</strong> بيتبع وظيفة الأصل: ٣١٣٧ للنشاط، ٣٢٠٦ للبيع
والتوزيع، ٣٣١٦ للعمومية والإدارية.
</div>
</div>
<div class="card">
<div class="table-responsive">
<table class="table" style="font-size:12.5px;">
<thead>
<tr>
<th>الفئة</th>
<th>حساب الأصل</th>
<th>مجمع الإهلاك</th>
<th>مصروف الإهلاك</th>
<th>الطريقة</th>
<th>العمر</th>
<th>أصول</th>
<th>القيمة الدفترية</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($categories as $c): ?>
<?php $inactive = (int) $c['is_active'] !== 1; ?>
<tr style="<?= $inactive ? 'opacity:.55;' : '' ?>">
<td style="font-weight:600;">
<?= e((string) $c['name_ar']) ?>
<?php if ($inactive): ?><span class="badge">موقوفة</span><?php endif; ?>
</td>
<td>
<?php if ($c['asset_code']): ?>
<code><?= e((string) $c['asset_code']) ?></code>
<?php else: ?>
<span style="color:#DC2626;">ناقص</span>
<?php endif; ?>
</td>
<td>
<?php if ($c['accum_code']): ?>
<code><?= e((string) $c['accum_code']) ?></code>
<?php else: ?>
<span style="color:#9CA3AF;">— لا تُهلك</span>
<?php endif; ?>
</td>
<td>
<?php if ($c['expense_code']): ?>
<code><?= e((string) $c['expense_code']) ?></code>
<?php else: ?>
<span style="color:#9CA3AF;"></span>
<?php endif; ?>
</td>
<td><?= (string) $c['depreciation_method'] === 'declining_balance' ? 'قسط متناقص' : 'قسط ثابت' ?></td>
<td><?= number_format((int) $c['default_useful_life_months']) ?> شهر</td>
<td><?= number_format((int) $c['asset_count']) ?></td>
<td><?= money((string) $c['book_value']) ?></td>
<td>
<?php if (can('asset.manage')): ?>
<a class="btn btn-sm btn-outline" href="/inventory/asset-categories/<?= (int) $c['id'] ?>/edit">تعديل</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
...@@ -111,24 +111,30 @@ $__template->layout('Layout.main'); ...@@ -111,24 +111,30 @@ $__template->layout('Layout.main');
<?php if (!$isEdit): ?> <?php if (!$isEdit): ?>
<div class="form-group" style="margin-bottom:18px;"> <div class="form-group" style="margin-bottom:18px;">
<label class="form-label">الأصل ده جاي منين؟ <span style="color:#DC2626;">*</span></label> <label class="form-label">الأصل ده جاي منين؟ <span style="color:#DC2626;">*</span></label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:6px;"> <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-top:6px;">
<label style="border:1px solid #E5E7EB;border-radius:8px;padding:12px 14px;cursor:pointer;display:block;"> <label style="border:1px solid #E5E7EB;border-radius:8px;padding:12px 14px;cursor:pointer;display:block;">
<input type="radio" name="acquisition_source" value="purchase" checked <input type="radio" name="acquisition_source" value="purchase" checked onchange="srcChanged(this.value)">
onchange="document.getElementById('payRow').style.display='';">
<strong style="font-size:13px;">شراء جديد</strong> <strong style="font-size:13px;">شراء جديد</strong>
<div style="color:#6B7280;font-size:11.5px;line-height:1.8;margin-top:4px;"> <div style="color:#6B7280;font-size:11.5px;line-height:1.8;margin-top:4px;">
هيتعمل قيد: <span style="direction:ltr;display:inline-block;">من ح/ الأصل الثابت — إلى ح/ النقدية أو الموردين</span> هيتعمل قيد: <span style="direction:ltr;display:inline-block;">من ح/ الأصل الثابت — إلى ح/ النقدية أو الموردين</span>
</div> </div>
</label> </label>
<label style="border:1px solid #E5E7EB;border-radius:8px;padding:12px 14px;cursor:pointer;display:block;"> <label style="border:1px solid #E5E7EB;border-radius:8px;padding:12px 14px;cursor:pointer;display:block;">
<input type="radio" name="acquisition_source" value="opening" <input type="radio" name="acquisition_source" value="opening" onchange="srcChanged(this.value)">
onchange="document.getElementById('payRow').style.display='none';">
<strong style="font-size:13px;">رصيد افتتاحي — موجود في الدفاتر</strong> <strong style="font-size:13px;">رصيد افتتاحي — موجود في الدفاتر</strong>
<div style="color:#6B7280;font-size:11.5px;line-height:1.8;margin-top:4px;"> <div style="color:#6B7280;font-size:11.5px;line-height:1.8;margin-top:4px;">
<strong>مش هيتعمل أي قيد.</strong> التكلفة مقيّدة أصلًا، والتسجيل هنا <strong>مش هيتعمل أي قيد.</strong> التكلفة مقيّدة أصلًا، والتسجيل هنا
عشان الأصل يبدأ يتهلك من دلوقتي. عشان الأصل يبدأ يتهلك من دلوقتي.
</div> </div>
</label> </label>
<label style="border:1px solid #E5E7EB;border-radius:8px;padding:12px 14px;cursor:pointer;display:block;">
<input type="radio" name="acquisition_source" value="capitalization" onchange="srcChanged(this.value)">
<strong style="font-size:13px;">رسملة مشروع تحت التنفيذ</strong>
<div style="color:#6B7280;font-size:11.5px;line-height:1.8;margin-top:4px;">
المشروع خلص وبقى جاهز للاستخدام. التكلفة بتتنقل من
«مشروعات تحت التنفيذ» لحساب الأصل ويبدأ الإهلاك.
</div>
</label>
</div> </div>
</div> </div>
...@@ -140,6 +146,28 @@ $__template->layout('Layout.main'); ...@@ -140,6 +146,28 @@ $__template->layout('Layout.main');
<option value="bank">تحويل بنكي</option> <option value="bank">تحويل بنكي</option>
</select> </select>
</div> </div>
<div class="form-group" id="cipRow" style="margin-bottom:18px;display:none;">
<label class="form-label">المشروع اللي التكلفة هتتنقل منه <span style="color:#DC2626;">*</span></label>
<select name="cip_account_id" class="form-input" style="max-width:520px;">
<option value="">— اختار حساب المشروع —</option>
<?php foreach (($cipAccounts ?? []) as $a): ?>
<option value="<?= (int) $a['id'] ?>">
<?= e((string) $a['account_code']) ?><?= e((string) $a['name_ar']) ?>
(رصيد <?= money((string) $a['current_balance']) ?>)
</option>
<?php endforeach; ?>
</select>
<small style="color:#6B7280;line-height:1.8;display:block;margin-top:4px;">
القيد هيبقى: <span style="direction:ltr;display:inline-block;">من ح/ الأصل الثابت — إلى ح/ المشروع تحت التنفيذ</span>.
<strong>مفيش فلوس بتتصرف هنا</strong> — الفلوس اتصرفت وقت التنفيذ.
</small>
<?php if (empty($cipAccounts)): ?>
<small style="color:#B45309;display:block;margin-top:6px;">
مفيش حسابات مشروعات تحت التنفيذ قابلة للترحيل في دليل الحسابات.
</small>
<?php endif; ?>
</div>
<?php endif; ?> <?php endif; ?>
<?php if ($locked): ?> <?php if ($locked): ?>
...@@ -225,6 +253,17 @@ $__template->layout('Layout.main'); ...@@ -225,6 +253,17 @@ $__template->layout('Layout.main');
</form> </form>
<script> <script>
// Each acquisition route asks for a different thing: a purchase asks how it was
// paid, a capitalisation asks which project it came out of, an opening balance
// asks nothing because it posts nothing.
function srcChanged(v) {
document.getElementById('payRow').style.display = v === 'purchase' ? '' : 'none';
var cip = document.getElementById('cipRow');
if (cip) { cip.style.display = v === 'capitalization' ? '' : 'none'; }
var sel = document.querySelector('select[name="cip_account_id"]');
if (sel) { sel.required = v === 'capitalization'; }
}
// Picking a category fills in that class's policy, so the accountant is not // Picking a category fills in that class's policy, so the accountant is not
// retyping the club's own depreciation rules for every desk and laptop. // retyping the club's own depreciation rules for every desk and laptop.
document.getElementById('categorySelect')?.addEventListener('change', function () { document.getElementById('categorySelect')?.addEventListener('change', function () {
......
...@@ -53,6 +53,7 @@ MenuRegistry::register('inventory', [ ...@@ -53,6 +53,7 @@ MenuRegistry::register('inventory', [
['label_ar' => 'الموردين', 'label_en' => 'Suppliers', 'route' => '/inventory/suppliers', 'permission' => 'supplier.view', 'order' => 7], ['label_ar' => 'الموردين', 'label_en' => 'Suppliers', 'route' => '/inventory/suppliers', 'permission' => 'supplier.view', 'order' => 7],
['label_ar' => 'أوامر الشراء', 'label_en' => 'Purchase Orders', 'route' => '/inventory/purchase-orders', 'permission' => 'purchase.view', 'order' => 8], ['label_ar' => 'أوامر الشراء', 'label_en' => 'Purchase Orders', 'route' => '/inventory/purchase-orders', 'permission' => 'purchase.view', 'order' => 8],
['label_ar' => 'الأصول والإهلاك', 'label_en' => 'Assets', 'route' => '/inventory/assets', 'permission' => 'asset.view', 'order' => 9], ['label_ar' => 'الأصول والإهلاك', 'label_en' => 'Assets', 'route' => '/inventory/assets', 'permission' => 'asset.view', 'order' => 9],
['label_ar' => 'فئات الأصول', 'label_en' => 'Asset Categories', 'route' => '/inventory/asset-categories', 'permission' => 'asset.view', 'order' => 10],
['label_ar' => 'عهدة الأصول', 'label_en' => 'Asset Custody', 'route' => '/inventory/assets/custody', 'permission' => 'asset.view', 'order' => 10], ['label_ar' => 'عهدة الأصول', 'label_en' => 'Asset Custody', 'route' => '/inventory/assets/custody', 'permission' => 'asset.view', 'order' => 10],
['label_ar' => 'قوائم المواد (BOM)', 'label_en' => 'Bill of Materials', 'route' => '/inventory/bom', 'permission' => 'inventory.bom.view','order' => 11], ['label_ar' => 'قوائم المواد (BOM)', 'label_en' => 'Bill of Materials', 'route' => '/inventory/bom', 'permission' => 'inventory.bom.view','order' => 11],
['label_ar' => 'أرصدة افتتاحية', 'label_en' => 'Opening Balances', 'route' => '/inventory/opening-balances', 'permission' => 'inventory.opening.manage', 'order' => 12], ['label_ar' => 'أرصدة افتتاحية', 'label_en' => 'Opening Balances', 'route' => '/inventory/opening-balances', 'permission' => 'inventory.opening.manage', 'order' => 12],
......
...@@ -3,28 +3,30 @@ ...@@ -3,28 +3,30 @@
> **لمين الدليل ده؟** للمحاسب اللي هيمسك النظام. مش دليل برمجة — ده كرّاسة تشغيل: > **لمين الدليل ده؟** للمحاسب اللي هيمسك النظام. مش دليل برمجة — ده كرّاسة تشغيل:
> كل خطوة مكتوب فيها **إنت رايح فين على الشاشة**، **بتضغط إيه**، و**القيد اللي هيتعمل**. > كل خطوة مكتوب فيها **إنت رايح فين على الشاشة**، **بتضغط إيه**، و**القيد اللي هيتعمل**.
> >
> **بيغطي إيه؟** الأساسيات المحاسبية كلها: التأسيس، الدورة اليومية، الشهرية، السنوية، > **بيغطي إيه؟** الأساسيات المحاسبية كلها + **كل أداة وويزرد في النظام** خطوة بخطوة.
> الأصول الثابتة والإهلاك، الرواتب، المخزون، الضرائب، الإقفال، والتقارير.
> >
> **لو عايز تفهم مسار الفلوس والاستحقاقات بالتفصيل** → [الدليل التاني](./دليل-مسار-الفلوس-والاستحقاقات.md). > **الدليل التاني:** [مسار الفلوس والاستحقاقات](./دليل-مسار-الفلوس-والاستحقاقات.md) —
> فيه تفاصيل أعمق عن توزيع الإيرادات والحسابات الوسيطة.
--- ---
## المحتويات ## المحتويات
| # | القسم | إمتى تعمله | | # | القسم | إمتى |
|---|---|---| |---|---|---|
| ٠ | [الصورة الكبيرة](#٠--الصورة-الكبيرة) | اقراه الأول | | ٠ | [الصورة الكبيرة](#٠--الصورة-الكبيرة) | اقراه الأول |
| ١ | [التأسيس — مرة واحدة](#١--التأسيس--مرة-واحدة-بس) | أول ما تمسك النظام | | ١ | [التأسيس — مرة واحدة](#١--التأسيس--مرة-واحدة-بس) | أول ما تمسك النظام |
| ٢ | [الأصول الثابتة والإهلاك](#٢--الأصول-الثابتة-والإهلاك) | تأسيس + كل شهر | | ٢ | [الأصول الثابتة والإهلاك](#٢--الأصول-الثابتة-والإهلاك) | تأسيس + كل شهر |
| ٣ | [الدورة اليومية](#٣--الدورة-اليومية) | كل يوم | | ٣ | [إعادة تبويب الحسابات](#٣--إعادة-تبويب-الحسابات) | لما رصيد يبقى في حساب غلط |
| ٤ | [الدورة الشهرية](#٤--الدورة-الشهرية) | آخر كل شهر | | ٤ | [الدورة اليومية](#٤--الدورة-اليومية) | كل يوم |
| ٥ | [الرواتب](#٥--الرواتب) | كل شهر | | ٥ | [الدورة الشهرية](#٥--الدورة-الشهرية) | آخر كل شهر |
| ٦ | [المخزون وتكلفة المبيعات](#٦--المخزون-وتكلفة-المبيعات) | مستمر | | ٦ | [الرواتب](#٦--الرواتب) | كل شهر |
| ٧ | [الضرائب](#٧--الضرائب) | شهري / سنوي | | ٧ | [المخزون وتكلفة المبيعات](#٧--المخزون-وتكلفة-المبيعات) | مستمر |
| ٨ | [الإقفال السنوي](#٨--الإقفال-السنوي) | آخر السنة | | ٨ | [الضرائب](#٨--الضرائب) | شهري / سنوي |
| ٩ | [التقارير — إزاي تتأكد إن الدفاتر سليمة](#٩--التقارير-وفحص-سلامة-الدفاتر) | مستمر | | ٩ | [الإقفال السنوي](#٩--الإقفال-السنوي) | آخر السنة |
| ١٠ | [حاجات محتاجة قرار منك دلوقتي](#١٠--حاجات-محتاجة-قرار-منك-دلوقتي) | **اقراه قبل الاجتماع** | | ١٠ | [التقارير وفحص سلامة الدفاتر](#١٠--التقارير-وفحص-سلامة-الدفاتر) | مستمر |
| ١١ | [**فهرس كل الأدوات والويزردز**](#١١--فهرس-كل-الأدوات-والويزردز) | مرجع |
| ١٢ | [حاجات محتاجة قرار منك](#١٢--حاجات-محتاجة-قرار-منك) | **اقراه قبل الاجتماع** |
--- ---
...@@ -50,152 +52,140 @@ flowchart TB ...@@ -50,152 +52,140 @@ flowchart TB
**يعني إيه؟** لو حساب اتغيّر في شاشة **توزيع الإيرادات**، كل القيود الجاية هتروح **يعني إيه؟** لو حساب اتغيّر في شاشة **توزيع الإيرادات**، كل القيود الجاية هتروح
للحساب الجديد — من غير ما حد يلمس كود. للحساب الجديد — من غير ما حد يلمس كود.
### الأربع دفاتر اللي لازم تبص عليهم ### القواعد اللي النظام بيفرضها عليك
```mermaid | القاعدة | النظام بيعمل إيه |
flowchart LR |---|---|
subgraph D["الدفاتر"] | القيد لازم يتوازن | بيرفض القيد ويقولك الفرق |
TB["ميزان المراجعة<br/>مدين = دائن"] | مينفعش ترحّل على حساب رئيسي | بيرفض — إلا أداة إعادة التبويب |
IS["قائمة الدخل<br/>إيراد − مصروف"] | مينفعش ترحّل على حساب موقوف | بيرفض — إلا قيد الإقفال السنوي |
BS["الميزانية<br/>أصول = التزامات + حقوق ملكية"] | مينفعش ترحّل في فترة مقفولة | بيرفض — إلا قيد الإقفال السنوي |
GL["دفتر الأستاذ<br/>حركة كل حساب"] | مينفعش ترحّل في سنة مالية مقفولة | بيرفض دايمًا |
end
TB --> IS
TB --> BS
GL --> TB
```
--- ---
## ١ — التأسيس — مرة واحدة بس ## ١ — التأسيس — مرة واحدة بس
الترتيب ده **مش اختياري**. كل خطوة بتعتمد على اللي قبلها.
```mermaid ```mermaid
flowchart TB flowchart TB
S1["١ دليل الحسابات"] --> S2["٢ السنة المالية"] S1["١ دليل الحسابات"] --> S2["٢ السنة المالية"]
S2 --> S3["٣ الحسابات البنكية والخزن"] S2 --> S3["٣ الحسابات البنكية"]
S3 --> S4["٤ القيود الافتتاحية"] S3 --> S4["٤ القيود الافتتاحية"]
S4 --> S5["٥ توزيع الإيرادات"] S4 --> S5["٥ توزيع الإيرادات"]
S5 --> S6["٦ سجل الأصول الثابتة"] S5 --> S6["٦ فئات الأصول"]
S6 --> S7["٧ فحص التوازن"] S6 --> S7["٧ سجل الأصول"]
S7 --> S8["٨ فحص التوازن"]
``` ```
### ١-١ — دليل الحسابات ### ١-١ — دليل الحسابات
**الشاشة:** `المحاسبة والدفتر العام ← دليل الحسابات``/accounting/chart-of-accounts` **الشاشة:** `المحاسبة ← دليل الحسابات``/accounting/chart-of-accounts`
الدليل **متظبط ومتسطّب بالكامل** بالفعل. متعملش حاجة غير إنك تتفرج وتتأكد. الدليل متظبط ومتسطّب بالكامل. متعملش حاجة غير إنك تتفرج.
القاعدة الوحيدة اللي لازم تعرفها:
| نوع الحساب | ممكن تقيّد عليه؟ | | نوع الحساب | ممكن تقيّد عليه؟ |
|---|---| |---|---|
| حساب **رئيسي** (Header) | ❌ لأ — ده عنوان مش حساب | | **رئيسي** (Header) | ❌ لأ — ده عنوان مش حساب |
| حساب **فرعي** (المستوى الأخير) | ✅ أيوه | | **فرعي** (المستوى الأخير) | ✅ أيوه |
| حساب **موقوف** | ❌ لأ — إلا قيد الإقفال السنوي | | **موقوف** | ❌ لأ |
> النظام بيرفض أي قيد على حساب رئيسي تلقائيًا ويقولك:
> «لا يمكن الترحيل إلى حساب رئيسي».
### ١-٢ — السنة المالية ### ١-٢ — السنة المالية
**الشاشة:** `المحاسبة ← السنوات المالية``/accounting/fiscal-years` **الشاشة:** `المحاسبة ← السنوات المالية``/accounting/fiscal-years`
**⚠️ فيه مشكلة مستنياك هنا.** بص على القسم [١٠](#١٠--حاجات-محتاجة-قرار-منك-دلوقتي). **خطوة بخطوة — إضافة سنة:**
1. اضغط **«+ سنة مالية جديدة»**.
2. حدد تاريخ البداية والنهاية.
3. احفظ.
الخطوات: **خطوة بخطوة — حل تداخل السنوات:**
1. افتح الشاشة. 1. افتح الشاشة. لو فيه تداخل هيظهر **تحذير أحمر فوق** فيه جدول بالسنين المتداخلة.
2. لو ظهرلك تحذير أحمر **«سنوات مالية متداخلة»** — ده لازم يتحل الأول. 2. قرر: السنة المالية للنادي **تقويمية (يناير–ديسمبر)** ولا **يوليو–يونيو**؟
3. اضغط **«+ سنة مالية جديدة»** لو محتاج سنة جديدة. 3. جنب اسم السنة اللي مش تابعة للنظام المعتمد اضغط **«أرشِف»**.
4. حدد السنة الحالية بـ **is_current**. 4. النظام هيرفض الأرشفة لو:
- السنة فيها **قيود** — لازم تنقلهم الأول (بيقولك عددهم)
- السنة هي **الحالية** — حدد سنة تانية كحالية الأول
```mermaid ```mermaid
flowchart LR flowchart TB
A["سنة مالية"] -->|"مفتوحة"| B["القيود مسموحة"] A["افتح السنوات المالية"] --> B{"فيه تحذير تداخل؟"}
A -->|"مقفولة"| C["القيود مرفوضة"] B -->|"لأ"| C["تمام"]
B --> D["إقفال شهري<br/>شهر شهر"] B -->|"أيوه"| D["قرر نظام السنة المالية"]
D --> E["إقفال سنوي<br/>بعد ما كل الشهور تتقفل"] D --> E["اضغط أرشِف على السنة الزيادة"]
E --> C E --> F{"فيها قيود؟"}
F -->|"أيوه"| G["النظام يرفض<br/>انقل القيود الأول"]
F -->|"لأ"| H{"هي الحالية؟"}
H -->|"أيوه"| I["النظام يرفض<br/>حدد سنة تانية كحالية"]
H -->|"لأ"| J["اتأرشفت<br/>مش هتدخل في تحديد سنة أي قيد"]
``` ```
### ١-٣ — الحسابات البنكية والخزن > **الأرشفة مش حذف.** السنة بتفضل موجودة في قاعدة البيانات، بس بتخرج من حسابات
> النظام لما يحدد قيد جديد تابع لأنهي سنة.
**الشاشة:** `المحاسبة ← الحسابات البنكية``/accounting/bank-accounts` ### ١-٣ — الحسابات البنكية
النظام عامل **٤ حسابات بنكية** مربوطة بحسابات الأستاذ: **الشاشة:** `المحاسبة ← الحسابات البنكية``/accounting/bank-accounts`
| البنك | حساب الأستاذ | النظام عامل ٤ حسابات مربوطة بحسابات الأستاذ ١٢٠٦٠٢٠١–٠٤.
|---|---|
| البنك الأهلي المصري | ١٢٠٦٠٢٠١ |
| بنك مصر | ١٢٠٦٠٢٠٢ |
| بنك القاهرة | ١٢٠٦٠٢٠٣ |
| بنك التعمير والإسكان | ١٢٠٦٠٢٠٤ |
> **🔴 مطلوب منك:** أرقام الحسابات دلوقتي **مؤقتة** ومكتوب فيها **🔴 خطوة مطلوبة:** الأرقام دلوقتي **مؤقتة** ومكتوب فيها «رقم الحساب غير محدد».
> «رقم الحساب غير محدد». النظام **ما ينفعش** يخترع رقم آيبان حقيقي. النظام **ما ينفعش** يخترع رقم آيبان. عدّلهم قبل أول إيداع.
> **عدّلهم بالأرقام الصح قبل أول إيداع.**
### ١-٤ — القيود الافتتاحية ### ١-٤ — القيود الافتتاحية
**الشاشة:** `المحاسبة ← القيود الافتتاحية``/accounting/opening-entries` **الشاشة:** `المحاسبة ← القيود الافتتاحية``/accounting/opening-entries`
القيد الافتتاحي **متعمل بالفعل** — ٩٠٬٦٠١٬٩٦٢٫٣٦ جنيه.
### ١-٥ — توزيع الإيرادات ### ١-٥ — توزيع الإيرادات
**الشاشة:** `المحاسبة ← توزيع الإيرادات``/accounting/revenue-mapping` **الشاشة:** `المحاسبة ← توزيع الإيرادات``/accounting/revenue-mapping`
هنا بتقول لكل نوع إيراد: **ينزل على أنهي حساب**. متظبط بالفعل. بتقول لكل نوع إيراد ينزل على أنهي حساب. التفاصيل في
[الدليل التاني](./دليل-مسار-الفلوس-والاستحقاقات.md).
للتفاصيل → [دليل مسار الفلوس](./دليل-مسار-الفلوس-والاستحقاقات.md).
### ١-٦ — سجل الأصول الثابتة ### ١-٦ إلى ١-٧ — الأصول
اقرا القسم [٢](#٢--الأصول-الثابتة-والإهلاك) — ده أهم شغل تأسيس متبقّي عليك. اقرا القسم [٢](#٢--الأصول-الثابتة-والإهلاك).
### ١-٧ — فحص التوازن ### ١-٨ — فحص التوازن
**الشاشة:** `المحاسبة ← ميزان المراجعة``/accounting/reports/trial-balance` **الشاشة:** `المحاسبة ← ميزان المراجعة``/accounting/reports/trial-balance`
لازم **مجموع المدين = مجموع الدائن**. دلوقتي: ٣٧٣٬٢٦٥٬٩٨١٫١٥ على الجهتين. ✅ لازم **مجموع المدين = مجموع الدائن**.
--- ---
## ٢ — الأصول الثابتة والإهلاك ## ٢ — الأصول الثابتة والإهلاك
### ليه ده أهم قسم في الدليل؟
الدفاتر فيها أصول ثابتة كبيرة:
| البند | المبلغ | الحساب |
|---|---|---|
| آلات ومعدات | ٤٬٧٠٥٬٦٨٦ | ١١٠١٠٣ |
| مشروعات تحت التنفيذ | ٤١٬٧٠٣٬٨٦٧ | ١١٠٣ |
| مجمع الإهلاك | (٥٧٩٬٠٤٨) | ٢٣٠١٠١ |
**بس سجل الأصول فاضي** — يعني **مفيش إهلاك اتحسب ولا مرة**. النتيجة:
الأرباح **مبالغ فيها**، والأصول في الميزانية **مبالغ فيها**.
### المفهوم ### المفهوم
```mermaid ```mermaid
flowchart TB flowchart TB
B["شراء أصل"] --> C["الأصل يتحمّل على الميزانية<br/>مش على المصروفات"] B["اقتناء أصل"] --> C["الأصل على الميزانية<br/>مش على المصروفات"]
C --> D["كل شهر: قسط إهلاك"] C --> D["كل شهر: قسط إهلاك"]
D --> E["مصروف إهلاك ← قائمة الدخل"] D --> E["مصروف إهلاك ← قائمة الدخل"]
D --> F["مجمع إهلاك ← يقلل الأصل في الميزانية"] D --> F["مجمع إهلاك ← يقلل الأصل"]
C --> G["الاستبعاد: بيع أو تخريد"] C --> G["الاستبعاد: بيع أو تخريد"]
G --> H["ربح أو خسارة استبعاد"] G --> H["ربح أو خسارة استبعاد"]
``` ```
> **القاعدة:** الأصل الثابت **مش مصروف**. النادي بدّل أصل بأصل. > **القاعدة:** الأصل الثابت **مش مصروف**. النادي بدّل أصل بأصل. التكلفة بتتحمّل
> التكلفة بتتحمّل على قائمة الدخل على مدى عمر الأصل عن طريق الإهلاك. > على قائمة الدخل على مدى عمر الأصل عن طريق الإهلاك.
### ٢-١ — فئات الأصول ### ٢-١ — فئات الأصول ← ابدأ من هنا
النظام عامل **٨ فئات** كل واحدة مربوطة بـ **٣ حسابات**: **الشاشة:** `المخازن ← فئات الأصول``/inventory/asset-categories`
**الفئة هي اللي بتوصّل الأصل بالدفاتر.** كل فئة شايلة **٣ حسابات**:
| الحساب | بيتستخدم إمتى |
|---|---|
| **حساب الأصل** | لما تسجّل الأصل — التكلفة بتنزل هنا |
| **مجمع الإهلاك** | كل شهر — بيزيد ويقلل الأصل في الميزانية |
| **مصروف الإهلاك** | كل شهر — بينزل على قائمة الدخل |
النظام عامل **٨ فئات** جاهزة:
| الفئة | حساب الأصل | مجمع الإهلاك | مصروف الإهلاك | العمر | | الفئة | حساب الأصل | مجمع الإهلاك | مصروف الإهلاك | العمر |
|---|---|---|---|---| |---|---|---|---|---|
...@@ -208,60 +198,105 @@ flowchart TB ...@@ -208,60 +198,105 @@ flowchart TB
| أجهزة كمبيوتر | ١١٠١٠٧٠١ | ٢٣٠١٠١٠٦ | ٣١٣٧٠٦ | ٣ سنين | | أجهزة كمبيوتر | ١١٠١٠٧٠١ | ٢٣٠١٠١٠٦ | ٣١٣٧٠٦ | ٣ سنين |
| أجهزة كهربائية | ١١٠١٠٨٠١ | ٢٣٠١٠١٠٧ | ٣١٣٧٠٧ | ٥ سنين | | أجهزة كهربائية | ١١٠١٠٨٠١ | ٢٣٠١٠١٠٧ | ٣١٣٧٠٧ | ٥ سنين |
> **الأراضي مالهاش إهلاك عن قصد** — الأرض ما بتستهلكش. **خطوة بخطوة — تعديل فئة:**
1. افتح `/inventory/asset-categories`.
2. لو فيه **تحذير أحمر** «فئات فيها أصول ومن غير حسابات إهلاك» — دي أهم حاجة تصلّحها.
3. اضغط **«تعديل»** جنب الفئة.
4. غيّر الحسابات من القوايم. **القوايم بتعرض بس الحسابات الفرعية النشطة**
الحساب الرئيسي مش هيظهر أصلًا.
5. احفظ.
**خطوة بخطوة — فئة جديدة:**
1. اضغط **«+ فئة جديدة»**.
2. اكتب الاسم.
3. اختار **حساب الأصل** (إجباري).
4. اختار **مجمع الإهلاك** و**مصروف الإهلاك****الاتنين مع بعض أو ولا واحد**.
5. حدد الطريقة والعمر الافتراضي.
6. احفظ.
> **⚠️ قاعدة مهمة:** لو حطيت مجمع الإهلاك من غير مصروف الإهلاك (أو العكس)،
> النظام بيرفض. القيد مش هيتوازن، والفئة كانت هتتخطّى بالكامل في الترحيل الشهري.
> >
> **مصروف الإهلاك** اتحط تحت **٣١٣٧ (تكاليف النشاط)** لأن مباني وملاعب ومعدات > **سيبهم الاتنين فاضيين بس للأصول اللي ما بتتهلكش** — زي الأراضي.
> النادي بتشتغل للنشاط. لو أصل بيخدم الإدارة، حوّل فئته لـ **٣٣١٦**، ولو بيخدم
> البيع والتوزيع حوّلها لـ **٣٢٠٦**. > **مصروف الإهلاك بيتبع وظيفة الأصل:**
> **٣١٣٧xx** للنشاط · **٣٢٠٦xx** للبيع والتوزيع · **٣٣١٦xx** للعمومية والإدارية.
> **تغيير الفئة بيأثر على الإهلاك الجاي بس.** القيود اللي اتقيّدت خلاص بتفضل زي
> ما هي — تعديلها معناه إعادة كتابة فترات مقفولة.
### ٢-٢ — تسجيل أصل — خطوة بخطوة ### ٢-٢ — تسجيل أصل — التلات طرق
**الشاشة:** `المخازن ← الأصول والإهلاك``/inventory/assets` **الشاشة:** `المخازن ← الأصول والإهلاك``/inventory/assets`
اضغط **«تسجيل أصل ثابت»** **«تسجيل أصل ثابت»**
```mermaid ```mermaid
flowchart TB flowchart TB
A["افتح /inventory/assets"] --> B["اضغط: تسجيل أصل ثابت"] A["اضغط: تسجيل أصل ثابت"] --> B["املا رقم الأصل والفئة"]
B --> C["املا رقم الأصل والفئة"] B --> C{"الأصل ده جاي منين؟"}
C --> D{"الأصل ده جاي منين؟"} C -->|"شراء جديد"| D["اختار: نقدًا / بنك / على الحساب"]
D -->|"شراء جديد"| E["اختار: اتدفع إزاي؟<br/>نقدًا / بنك / على الحساب"] C -->|"رصيد افتتاحي"| E["مفيش قيد خالص"]
D -->|"رصيد افتتاحي"| F["مفيش قيد<br/>التكلفة في الدفاتر أصلًا"] C -->|"رسملة مشروع"| F["اختار حساب المشروع"]
E --> G["قيد: من ح/ الأصل — إلى ح/ النقدية أو الموردين"] D --> G["من ح/ الأصل — إلى ح/ النقدية أو الموردين"]
F --> H["الأصل يبدأ يتهلك من دلوقتي"] E --> H["الأصل يبدأ يتهلك من دلوقتي"]
F --> I["من ح/ الأصل — إلى ح/ المشروع تحت التنفيذ"]
G --> H G --> H
I --> H
``` ```
#### 🔴 أهم اختيار في الشاشة دي #### 🔴 أهم اختيار في الشاشة
الشاشة هتسألك **«الأصل ده جاي منين؟»**، وده أهم سؤال:
| الاختيار | إمتى؟ | القيد | | الاختيار | إمتى؟ | القيد |
|---|---|---| |---|---|---|
| **شراء جديد** | النادي اشترى الأصل دلوقتي | `من ح/ الأصل الثابت` / `إلى ح/ النقدية أو الموردين` | | **شراء جديد** | النادي اشترى الأصل دلوقتي | `من ح/ الأصل` / `إلى ح/ النقدية أو الموردين` |
| **رصيد افتتاحي** | الأصل موجود في الدفاتر من زمان | **مفيش أي قيد** | | **رصيد افتتاحي** | الأصل موجود في الدفاتر من زمان | **مفيش أي قيد** |
| **رسملة مشروع تحت التنفيذ** | مشروع خلص وبقى جاهز للاستخدام | `من ح/ الأصل` / `إلى ح/ المشروع` |
> **ليه ده مهم؟**
> - لو الأصل في القيد الافتتاحي وسجّلته «شراء جديد» → **الميزانية هتتعدّ مرتين**.
> - لو مشروع خلص وسجّلته «شراء جديد» → **المصروف هيتعدّ مرتين**، لأن الفلوس
> اتصرفت خلاص وهي في «مشروعات تحت التنفيذ».
#### خطوات الرسملة بالتفصيل
> **ليه؟** لو الأصل موجود في القيد الافتتاحي وسجّلته «شراء جديد»، الميزانية 1. اضغط **«تسجيل أصل ثابت»**.
> هتتعدّ **مرتين**. الأصول اللي عند النادي دلوقتي (الـ٤٫٧ مليون آلات 2. اختار **الفئة** الصح للأصل الجاهز (مباني، آلات… إلخ).
> والـ٤١٫٧ مليون مشروعات) كلها **«رصيد افتتاحي»**. 3. اختار **«رسملة مشروع تحت التنفيذ»**.
4. من القايمة اللي هتظهر اختار **حساب المشروع** — القايمة بتوريك رصيد كل مشروع.
5. اكتب **التكلفة** المطلوب رسملتها.
6. حدد **تاريخ الجاهزية للاستخدام** — الإهلاك بيبدأ منه.
7. احفظ.
#### الحقول > **مفيش فلوس بتتصرف في الرسملة.** الفلوس اتصرفت وقت التنفيذ. الرسملة بس بتنقل
> التكلفة المتراكمة من حساب المشروع لحساب الأصل عشان يبدأ يتهلك.
#### حقول الشاشة
| الحقل | يتكتب فيه إيه | | الحقل | يتكتب فيه إيه |
|---|---| |---|---|
| رقم الأصل | بيتولّد لوحده — `FA-2026-0001` | | رقم الأصل | بيتولّد لوحده — `FA-2026-0001` |
| الفئة | **إجباري** — هي اللي بتحدد حسابات الإهلاك | | الفئة | **إجباري** — هي اللي بتحدد حسابات الإهلاك |
| المخزن | سيبه فاضي للمباني والملاعب والعربيات | | المخزن | سيبه فاضي للمباني والملاعب والعربيات |
| تاريخ الشراء | تاريخ الشراء الأصلي | | تاريخ الشراء | تاريخ الشراء الأصلي أو تاريخ الجاهزية للرسملة |
| التكلفة | التكلفة الأصلية كاملة | | التكلفة | التكلفة الأصلية كاملة |
| مجمع الإهلاك حتى تاريخه | للأصل القديم اللي اتهلك جزء منه قبل التسجيل | | مجمع الإهلاك حتى تاريخه | للأصل القديم اللي اتهلك جزء منه قبل التسجيل |
| القيمة التخريدية | الإهلاك بيقف عندها | | القيمة التخريدية | الإهلاك بيقف عندها |
| العمر الإنتاجي | بيتملّى من الفئة، وتقدر تغيّره | | العمر الإنتاجي | بيتملّى من الفئة، وتقدر تغيّره |
> **بعد أول قيد أو أول إهلاك، التكلفة ومجمع الإهلاك بيتقفلوا** في شاشة التعديل.
> أي تغيير عليهم بيتعمل **بقيد يومية** عشان السجل والدفاتر يفضلوا متطابقين.
### ٢-٣ — تشغيل الإهلاك الشهري ### ٢-٣ — تشغيل الإهلاك الشهري
**الشاشة:** `/inventory/assets` → اختار الشهر → اضغط **«تشغيل الإهلاك»** **الشاشة:** `/inventory/assets`
القيد اللي بيتعمل — **مجمّع بالفئة**، مش قيد لكل أصل: 1. من فوق، اختار **الشهر**.
2. اضغط **«تشغيل الإهلاك»**.
3. أكّد.
القيد بيتجمّع **بالفئة**، مش قيد لكل أصل:
``` ```
من ح/ ٣١٣٧٠٦ مصروف إهلاك — أجهزة كمبيوتر ١٬٠٠٠ من ح/ ٣١٣٧٠٦ مصروف إهلاك — أجهزة كمبيوتر ١٬٠٠٠
...@@ -270,16 +305,19 @@ flowchart TB ...@@ -270,16 +305,19 @@ flowchart TB
إلى ح/ ٢٣٠١٠١٠٢ مجمع إهلاك — آلات ١٠٬٠٠٠ إلى ح/ ٢٣٠١٠١٠٢ مجمع إهلاك — آلات ١٠٬٠٠٠
``` ```
> **بيتشغّل مرة واحدة للشهر.** لو ضغطت تاني بالغلط، النظام بيتجاهل — مفيش تكرار. > **بيتشغّل مرة واحدة للشهر.** لو ضغطت تاني بالغلط، النظام بيتجاهل.
### ٢-٤ — استبعاد أصل ### ٢-٤ — استبعاد أصل
**الشاشة:** `/inventory/assets/{رقم}`**«تسجيل التصرف»** **الشاشة:** `/inventory/assets/{رقم}`**«تسجيل التصرف»**
القيد بيشيل التكلفة **ومجمع إهلاكها** ويحسب الربح أو الخسارة: 1. افتح الأصل.
2. اضغط **«تسجيل التصرف»**.
3. اكتب **حصيلة البيع** (صفر لو تخريد) و**السبب**.
4. احفظ.
``` ```
مثال: أصل تكلفته ٣٦٬٠٠٠، مجمع إهلاكه ١٬٠٠٠، اتباع بـ ٣٠٬٠٠٠ مثال: تكلفة ٣٦٬٠٠٠، مجمع إهلاك ١٬٠٠٠، اتباع بـ ٣٠٬٠٠٠
من ح/ النقدية ٣٠٬٠٠٠ من ح/ النقدية ٣٠٬٠٠٠
من ح/ مجمع الإهلاك ١٬٠٠٠ من ح/ مجمع الإهلاك ١٬٠٠٠
...@@ -289,28 +327,88 @@ flowchart TB ...@@ -289,28 +327,88 @@ flowchart TB
--- ---
## ٣ — الدورة اليومية ## ٣ — إعادة تبويب الحسابات
**الشاشة:** `المحاسبة ← إعادة تبويب الحسابات``/accounting/reclassification`
### المشكلة اللي بتحلها
رصيد وقع في حساب غلط: قيد افتتاحي نزل على **حساب رئيسي**، أو تكلفة اتقيّدت على
الأب بدل الابن، أو حساب اتوقف وفيه رصيد.
**الرصيد ما ينفعش يتعدّل بالإيد.** الدفاتر هي السجل، ورصيد مختلف عن القيود اللي
وراه أسوأ من رصيد غلط.
```mermaid
flowchart TB
A["افتح إعادة تبويب الحسابات"] --> B["النظام بيعرض الأرصدة العالقة"]
B --> C["اضغط: انقل الرصيد"]
C --> D["اختار حساب الوجهة"]
D --> E["النظام بيعرض القيد قبل الترحيل"]
E --> F["اكتب السبب"]
F --> G["رحّل القيد"]
G --> H["الحساب القديم يبقى صفر<br/>والرصيد في الحساب الصح"]
```
### خطوة بخطوة
1. افتح `/accounting/reclassification`.
2. الشاشة بتعرض **الأرصدة العالقة** — الحسابات الرئيسية أو الموقوفة اللي فيها رصيد.
3. اضغط **«انقل الرصيد»** جنب الحساب.
4. من قايمة **«إلى حساب»** اختار الوجهة:
- الحسابات **الفرعية بتاعت نفس الحساب** بتظهر الأول
- بعدين حسابات تانية من **نفس النوع**
5. **المبلغ** — سيبه فاضي عشان تنقل الرصيد كله، أو اكتب جزء.
6. النظام بيوريك **القيد بالظبط** قبل ما ترحّل.
7. اكتب **السبب** (إجباري — بيفضل في الدفاتر).
8. اضغط **«رحّل القيد»**.
### اللي النظام بيمنعه
| المحاولة | الرد |
|---|---|
| الوجهة حساب **رئيسي** | ❌ «اختار حساب فرعي» |
| الوجهة حساب **موقوف** | ❌ مرفوض |
| نقل بين **نوعين مختلفين** (أصل ← إيراد) | ❌ «بيغيّر تصنيف الميزانية» |
| نفس الحساب مصدر ووجهة | ❌ مرفوض |
| مبلغ أكبر من الرصيد | ❌ بيقولك الرصيد الفعلي |
| من غير سبب | ❌ مرفوض |
> **الاتجاه بيتحسب لوحده.** الحساب اللي رصيده مدين بيتقفل بـ**دائن**، والعكس.
> دي أكتر حاجة بتتغلط في القيد اليدوي — لو عكستها بتضاعف الرصيد بدل ما تنقله.
> **القيد ده تقدر تعكسه** من شاشة قيود اليومية زي أي قيد تاني.
---
## ٤ — الدورة اليومية
```mermaid ```mermaid
flowchart TB flowchart TB
A["١ الحركات اليومية<br/>/accounting/daily-transactions"] --> B["٢ حركة النقدية اليومية<br/>/accounting/statements/daily-cash"] A["١ الحركات اليومية"] --> B["٢ حركة النقدية اليومية"]
B --> C["٣ فين الفلوس دلوقتي<br/>/accounting/posting-chains/parked"] B --> C["٣ فين الفلوس دلوقتي"]
C --> D["٤ سندات الصرف والقبض<br/>/accounting/vouchers"] C --> D["٤ سندات الصرف والقبض"]
``` ```
| # | الشاشة | بتعمل إيه | | # | الشاشة | الطريق | بتعمل إيه |
|---|---|---| |---|---|---|---|
| ١ | `الحركات اليومية` | كل حركة النهارده | | ١ | الحركات اليومية | `/accounting/daily-transactions` | كل حركة النهارده |
| ٢ | `حركة النقدية اليومية` | الداخل والخارج نقدًا | | ٢ | حركة النقدية اليومية | `/accounting/statements/daily-cash` | الداخل والخارج نقدًا |
| ٣ | `فين الفلوس دلوقتي` | فلوس واقفة في حسابات وسيطة | | ٣ | فين الفلوس دلوقتي | `/accounting/posting-chains/parked` | فلوس واقفة في حسابات وسيطة |
| ٤ | `سندات الصرف والقبض` | صرف أو قبض بره الدورة العادية | | ٤ | سندات الصرف والقبض | `/accounting/vouchers` | صرف أو قبض بره الدورة |
> **«فين الفلوس دلوقتي»** أهم شاشة يومية: بتوريك الفلوس اللي اتحصّلت وما وصلتش > **«فين الفلوس دلوقتي»** أهم شاشة يومية: بتوريك الفلوس اللي اتحصّلت وما وصلتش
> البنك لسه. لو رقم قعد واقف كتير — فيه حاجة غلط. > البنك. لو رقم قعد واقف كتير — فيه حاجة غلط.
### القيد اليدوي ### القيد اليدوي
**الشاشة:** `المحاسبة ← قيود اليومية``/accounting/journal-entries`**«قيد جديد»** **الشاشة:** `/accounting/journal-entries`**«قيد جديد»**
1. اضغط **«قيد جديد»**.
2. حدد **التاريخ**.
3. ضيف السطور: الحساب + مدين أو دائن.
4. اكتب **الوصف**.
5. **رحّل**.
```mermaid ```mermaid
flowchart LR flowchart LR
...@@ -318,73 +416,82 @@ flowchart LR ...@@ -318,73 +416,82 @@ flowchart LR
B --> C{"مدين = دائن؟"} B --> C{"مدين = دائن؟"}
C -->|"لأ"| D["النظام بيرفض"] C -->|"لأ"| D["النظام بيرفض"]
C -->|"أيوه"| E{"الفترة مفتوحة؟"} C -->|"أيوه"| E{"الفترة مفتوحة؟"}
E -->|"لأ"| F["النظام بيرفض"] E -->|"لأ"| D
E -->|"أيوه"| G["ترحيل"] E -->|"أيوه"| F["ترحيل"]
``` ```
النظام بيرفض القيد لو: مش متوازن، أو على حساب رئيسي، أو على حساب موقوف،
أو في فترة مقفولة، أو في سنة مالية مقفولة.
--- ---
## ٤ — الدورة الشهرية ## ٥ — الدورة الشهرية
الترتيب ده **مهم**:
```mermaid ```mermaid
flowchart TB flowchart TB
M1["١ فحص الاستحقاقات<br/>/accounting/accruals"] --> M2["٢ سد الفجوات<br/>/accounting/gaps"] M1["١ فحص الاستحقاقات"] --> M2["٢ سد الفجوات"]
M2 --> M3["٣ الرواتب<br/>/hr/payroll"] M2 --> M3["٣ الرواتب"]
M3 --> M4["٤ الإهلاك<br/>/inventory/assets"] M3 --> M4["٤ الإهلاك"]
M4 --> M5["٥ المطابقة البنكية<br/>/accounting/bank-reconciliation"] M4 --> M5["٥ المطابقة البنكية"]
M5 --> M6["٦ ميزان المراجعة<br/>/accounting/reports/trial-balance"] M5 --> M6["٦ ميزان المراجعة"]
M6 --> M7["٧ إقفال الشهر<br/>/accounting/period-closing"] M6 --> M7["٧ إقفال الشهر"]
``` ```
### ٤-١ — فحص الاستحقاقات ### ٥-١ — فحص الاستحقاقات
**الشاشة:** `/accounting/accruals`
1. افتح الشاشة — بتوريك المطالبات المستحقة اللي لسه ما اتقيّدتش.
2. اضغط **«شغّل الفحص»**.
3. النظام بيقيّد: `من ح/ المدينون` / `إلى ح/ الإيراد`.
> بيتشغّل أكتر من مرة براحتك — المطالبة اللي اتقيّدت مرة ما بتتقيّدش تاني.
**الشاشة:** `المحاسبة ← الاستحقاقات``/accounting/accruals` ### ٥-٢ — سد الفجوات
بيقيّد الفلوس **اللي لينا وما اتحصّلتش** لسه. اتشغّل بالفعل: **١٬٣١٦ مطالبة = **الشاشة:** `/accounting/gaps`
٨٣٥٬١٦٧٫٩٣ جنيه**.
### ٤-٢ — سد الفجوات الحاجات اللي النظام مش عارف يسعّرها لوحده:
**الشاشة:** `المحاسبة ← سد الفجوات``/accounting/gaps` 1. **تحديد تسعيرة:** اختار البند → اكتب السعر → شوف **هينزل كام** → احفظ.
2. **نقل عقود الأكاديميات:** اختار العقود → حدد شروط التسوية → انقل.
3. **لاعبين مربوطين بعضو مش موجود:** اضغط **«صلّح الربط»** → بيوديك لملف اللاعب.
الحاجات اللي النظام **مش عارف يسعّرها لوحده** ومحتاج قرار منك. > **مفيش حاجة بتتقيّد من الشاشة دي على طول.** إنت بتسجّل قرار، وماسح
> الاستحقاقات بينفّذه في أول جولة بعد كده.
### ٤-٣ إلى ٤-٤ — الرواتب والإهلاك ### ٥-٣ إلى ٥-٤ — الرواتب والإهلاك
اقرا القسم [٥](--الرواتب) والقسم [٢-٣](#٢-٣--تشغيل-الإهلاك-الشهري). القسم [٦](--الرواتب) والقسم [٢-٣](#٢-٣--تشغيل-الإهلاك-الشهري).
### ٤-٥ — المطابقة البنكية ### ٥-٥ — المطابقة البنكية
**الشاشة:** `المحاسبة ← المطابقة البنكية``/accounting/bank-reconciliation` **الشاشة:** `/accounting/bank-reconciliation`
طابق كشف البنك مع الدفاتر. 1. اختار الحساب البنكي والفترة.
2. ادخل رصيد كشف البنك.
3. طابق الحركات.
4. اعتمد المطابقة.
### ٤-٦ — ميزان المراجعة ### ٥-٦ — ميزان المراجعة
**لازم** مدين = دائن قبل الإقفال. **لازم** مدين = دائن قبل الإقفال.
### ٤-٧ — إقفال الشهر ### ٥-٧ — إقفال الشهر
**الشاشة:** `المحاسبة ← إقفال الفترات``/accounting/period-closing` **الشاشة:** `/accounting/period-closing`
```mermaid 1. اختار **السنة المالية** و**الشهر**.
flowchart LR 2. اضغط **«إقفال الفترة»**.
A["اختار الشهر"] --> B["اضغط إقفال"] 3. أي قيد جديد في الشهر ده هيترفض.
B --> C["الشهر يتقفل"]
C --> D["أي قيد جديد<br/>في الشهر ده يترفض"]
D --> E["محتاج تعديل؟<br/>افتح الفترة تاني بسبب مكتوب"]
```
> **الإقفال مش نهائي.** تقدر تفتح الشهر تاني، بس لازم تكتب **سبب** — وده بيتسجّل. **لو محتاج تعدّل بعد الإقفال:**
1. اضغط **«إعادة فتح»**.
2. **اكتب السبب** — إجباري وبيتسجّل.
3. عدّل.
4. اقفل تاني.
--- ---
## ٥ — الرواتب ## ٦ — الرواتب
**الشاشة:** `الموارد البشرية ← كشوف الرواتب``/hr/payroll` **الشاشة:** `الموارد البشرية ← كشوف الرواتب``/hr/payroll`
...@@ -396,7 +503,10 @@ flowchart TB ...@@ -396,7 +503,10 @@ flowchart TB
D --> E["قيد الرواتب تلقائي"] D --> E["قيد الرواتب تلقائي"]
``` ```
القيد: 1. اعمل **فترة رواتب** للشهر.
2. اضغط **حساب المسير** — النظام بيحسب الأساسي والبدلات والتأمينات والضريبة.
3. راجع المسير.
4. اضغط **صرف**.
``` ```
من ح/ ٣١٠١٠١ الأجور الأساسية ١٥٬٠٠٠ من ح/ ٣١٠١٠١ الأجور الأساسية ١٥٬٠٠٠
...@@ -406,12 +516,14 @@ flowchart TB ...@@ -406,12 +516,14 @@ flowchart TB
إلى ح/ ٢٣٠٨٠٤٠٣ ضريبة كسب العمل ٩٬٢١١٫٨٠ إلى ح/ ٢٣٠٨٠٤٠٣ ضريبة كسب العمل ٩٬٢١١٫٨٠
``` ```
> **مهم:** القيد بيتعمل عند **الصرف** مش عند الحساب. دلوقتي فيه **٣ مسيرات > **القيد بيتعمل عند الصرف مش عند الحساب.** المسير المحسوب ومش مصروف = مفيش
> محسوبة وما اتصرفتش** — عشان كده مفيش مصروف رواتب في الدفاتر. > مصروف رواتب في الدفاتر.
>
> **الصرف مرتين بالغلط ما بيقيّدش مرتين** — النظام بيتجاهل التاني.
--- ---
## ٦ — المخزون وتكلفة المبيعات ## ٧ — المخزون وتكلفة المبيعات
```mermaid ```mermaid
flowchart TB flowchart TB
...@@ -426,153 +538,183 @@ flowchart TB ...@@ -426,153 +538,183 @@ flowchart TB
|---|---| |---|---|
| الأصناف | `/inventory/items` | | الأصناف | `/inventory/items` |
| حركات المخزون | `/inventory/movements` | | حركات المخزون | `/inventory/movements` |
| النقل بين المخازن | `/inventory/transfers` |
| الجرد | `/inventory/audits` | | الجرد | `/inventory/audits` |
| أرصدة افتتاحية | `/inventory/opening-balances` | | أرصدة افتتاحية | `/inventory/opening-balances` |
| أوامر الشراء | `/inventory/purchase-orders` |
**الجرد خطوة بخطوة:**
1. `/inventory/audits`**«جرد جديد»**.
2. اختار المخزن.
3. **«بدء العد»** → ادخل الكميات الفعلية.
4. **«اعتماد»** → الفرق بيتقيّد تسوية مخزون.
--- ---
## ٧ — الضرائب ## ٨ — الضرائب
| الضريبة | الحساب | الشاشة | | الضريبة | الحساب | من فين |
|---|---|---| |---|---|---|
| القيمة المضافة | ٢٣٠٨٠٤٠٤ | `/accounting/reports/trial-balance` | | القيمة المضافة | ٢٣٠٨٠٤٠٤ | تلقائي مع المبيعات |
| كسب العمل | ٢٣٠٨٠٤٠٣ | `/hr/payroll` | | كسب العمل | ٢٣٠٨٠٤٠٣ | `/hr/payroll` |
| التمغة العادية | ٢٣٠٨١٢٠٢ | تلقائي مع الاستمارات | | التمغة العادية | ٢٣٠٨١٢٠٢ | تلقائي مع الاستمارات |
| التمغة الإضافية | ٢٣٠٨١٢٠٣ | تلقائي | | التمغة الإضافية | ٢٣٠٨١٢٠٣ | تلقائي |
| الخصم من المنبع | ٢٣٠٨٠٦ | مع مدفوعات الموردين | | الخصم من المنبع | ٢٣٠٨٠٦ | مع مدفوعات الموردين |
> **⚠️ ملحوظة:** حساب القيمة المضافة ٢٣٠٨٠٤٠٤ رصيده **مدين ٤٬٧٦٨٬٨٤٤** من القيد
> الافتتاحي — يعني ضريبة **مستحقة للنادي** مش عليه. راجع ده مع مكتب الضرايب.
--- ---
## ٨ — الإقفال السنوي ## ٩ — الإقفال السنوي
```mermaid ```mermaid
flowchart TB flowchart TB
A["١ اقفل كل شهور السنة"] --> B["٢ راجع ميزان المراجعة"] A["١ اقفل كل شهور السنة"] --> B["٢ راجع ميزان المراجعة"]
B --> C["٣ اضغط إقفال السنة المالية"] B --> C["٣ اضغط إقفال السنة المالية"]
C --> D["قيد الإقفال"] C --> D["قيد الإقفال"]
D --> E["كل حسابات الإيراد تتقفل بمدين"] D --> E["حسابات الإيراد تتقفل بمدين"]
D --> F["كل حسابات المصروف تتقفل بدائن"] D --> F["حسابات المصروف تتقفل بدائن"]
E --> G["الفرق ← ٢١٠٢٠١ أرباح مرحلة"] E --> G["الفرق ← ٢١٠٢٠١ أرباح مرحلة"]
F --> G F --> G
G --> H["السنة المالية تبقى مقفولة"] G --> H["السنة تبقى مقفولة"]
``` ```
**الشاشة:** `/accounting/period-closing`**«إقفال السنة المالية»** **الشاشة:** `/accounting/period-closing`**«إقفال السنة المالية»**
القيد (مثال من التجربة على نسخة من الدفاتر الحقيقية): 1. اتأكد إن **كل الشهور مقفولة** — النظام بيرفض ويقولك الشهور الناقصة بالاسم.
2. راجع ميزان المراجعة.
3. اضغط **«إقفال السنة المالية»**.
``` ```
من ح/ ٤١٠٥١٥ إيرادات متنوعة ٤٬٢٨٠٬٩٩٩٫٩٦ من ح/ ٤١٠٥١٥ إيرادات متنوعة ٤٬٢٨٠٬٩٩٩٫٩٦
من ح/ ٤١٠١٠٢ إضافة عضويات ٢٬٠٧٨٬٨٠٤٫٠٠ من ح/ ٤١٠١٠٢ إضافة عضويات ٢٬٠٧٨٬٨٠٤٫٠٠
من ح/ ٤١٠١٠١ عضويات جديدة ١٬٧٠٧٬١٤٥٫٠٠
... باقي حسابات الإيراد والمصروف ... ... باقي حسابات الإيراد والمصروف ...
إلى ح/ ٢١٠٢٠١ أرباح مرحلة ٧٬١٥٧٬٤٧٦٫٢٦ إلى ح/ ٢١٠٢٠١ أرباح مرحلة ٧٬١٥٧٬٤٧٦٫٢٦
``` ```
> **النظام بيرفض الإقفال السنوي لو فيه شهر واحد مفتوح**، ويقولك الشهور بالاسم. > **قيد الإقفال هو الوحيد اللي مسموحله** يترحّل في فترة مقفولة وعلى حساب موقوف —
> لأنه بالظبط اللي بيقفلهم.
--- ---
## ٩ — التقارير وفحص سلامة الدفاتر ## ١٠ — التقارير وفحص سلامة الدفاتر
| التقرير | الطريق | بتدور على إيه | | التقرير | الطريق |
|---|---|---| |---|---|
| ميزان المراجعة | `/accounting/reports/trial-balance` | مدين = دائن | | ميزان المراجعة | `/accounting/reports/trial-balance` |
| قائمة الدخل | `/accounting/reports/income-statement` | إيراد − مصروف | | قائمة الدخل | `/accounting/reports/income-statement` |
| الميزانية العمومية | `/accounting/reports/balance-sheet` | أصول = التزامات + حقوق ملكية | | الميزانية العمومية | `/accounting/reports/balance-sheet` |
| دفتر الأستاذ | `/accounting/reports/general-ledger` | حركة حساب معيّن | | ميزانية موحدة | `/accounting/reports/consolidated-balance-sheet` |
| المدينون | `/accounting/reports/accounts-receivable` | اللي لينا | | دفتر الأستاذ | `/accounting/reports/general-ledger` |
| الدائنون | `/accounting/reports/accounts-payable` | اللي علينا | | المدينون | `/accounting/reports/accounts-receivable` |
| كشف حساب عضو | `/accounting/reports/member-statement` | حساب عضو | | الدائنون | `/accounting/reports/accounts-payable` |
| كشف حساب عضو | `/accounting/reports/member-statement` |
| الخزينة والمدفوعات | `/accounting/reports/treasury` |
| تحليل الإيرادات | `/accounting/reports/revenue-analysis` |
| كشف حساب عميل | `/accounting/statements/customer` |
| كشف حساب مورد | `/accounting/statements/supplier` |
### فحص شهري سريع ### فحص شهري سريع
```mermaid ```mermaid
flowchart TB flowchart TB
A["ميزان المراجعة"] --> B{"مدين = دائن؟"} A["ميزان المراجعة"] --> B{"مدين = دائن؟"}
B -->|"لأ"| C["🔴 وقف واتصل بالدعم"] B -->|"لأ"| C["وقف واتصل بالدعم"]
B -->|"أيوه"| D["الميزانية العمومية"] B -->|"أيوه"| D["الميزانية العمومية"]
D --> E{"أصول = التزامات + حقوق ملكية؟"} D --> E{"أصول = التزامات + حقوق ملكية؟"}
E -->|"لأ"| C E -->|"لأ"| C
E -->|"أيوه"| F["فين الفلوس دلوقتي"] E -->|"أيوه"| F["فين الفلوس دلوقتي"]
F --> G{"فيه فلوس واقفة من زمان؟"} F --> G{"فيه فلوس واقفة من زمان؟"}
G -->|"أيوه"| H["راجع التسويات"] G -->|"أيوه"| H["راجع التسويات"]
G -->|"لأ"| I["الدفاتر سليمة"] G -->|"لأ"| I["الدفاتر سليمة"]
``` ```
--- ---
## ١٠ — حاجات محتاجة قرار منك دلوقتي ## ١١ — فهرس كل الأدوات والويزردز
| # | الأداة | الطريق | بتعمل إيه |
|---|---|---|---|
| ١ | دليل الحسابات | `/accounting/chart-of-accounts` | شجرة الحسابات |
| ٢ | السنوات المالية | `/accounting/fiscal-years` | إضافة/إقفال/**أرشفة** السنين |
| ٣ | القيود الافتتاحية | `/accounting/opening-entries` | أرصدة أول المدة |
| ٤ | قيود اليومية | `/accounting/journal-entries` | قيد يدوي + عكس القيود |
| ٥ | أنواع اليومية | `/accounting/journal-types` | تصنيف القيود |
| ٦ | **إعادة تبويب الحسابات** | `/accounting/reclassification` | نقل رصيد من حساب لحساب بقيد |
| ٧ | إقفال الفترات | `/accounting/period-closing` | إقفال شهري وسنوي + إعادة فتح |
| ٨ | توزيع الإيرادات | `/accounting/revenue-mapping` | كل إيراد ينزل على أنهي حساب |
| ٩ | مركز التوصيل | `/accounting/revenue-mapping/connections` | البنود غير الموصّلة |
| ١٠ | الإيراد المؤجل | `/accounting/revenue-mapping/recognition` | الاعتراف بالإيراد على فترات |
| ١١ | مسار الفلوس | `/accounting/posting-chains` | سلاسل الترحيل والحسابات الوسيطة |
| ١٢ | فين الفلوس دلوقتي | `/accounting/posting-chains/parked` | فلوس واقفة في الطريق |
| ١٣ | إعادة تبويب الخزن | `/accounting/posting-chains/reclassification` | تصحيح حسابات الخزن |
| ١٤ | الاستحقاقات | `/accounting/accruals` | تقييد اللي لينا قبل التحصيل |
| ١٥ | سد الفجوات | `/accounting/gaps` | تسعيرات + عقود + ربط الأعضاء |
| ١٦ | رسوم الفروع | `/accounting/branch-fees` | توزيع رسوم الفروع |
| ١٧ | إقفال أوراق الدفع | `/accounting/notes-payable` | أوراق الدفع المستحقة |
| ١٨ | المطالبات والتحصيل | `/accounting/billing` | مطالبات الأعضاء |
| ١٩ | سندات الصرف والقبض | `/accounting/vouchers` | صرف وقبض |
| ٢٠ | الحسابات البنكية | `/accounting/bank-accounts` | البنوك وأرقام الحسابات |
| ٢١ | المطابقة البنكية | `/accounting/bank-reconciliation` | مطابقة كشف البنك |
| ٢٢ | الأوراق التجارية | `/accounting/instruments` | الشيكات والكمبيالات |
| ٢٣ | القروض البنكية | `/accounting/loans` | أقساط وفوايد القروض |
| ٢٤ | الاعتمادات المستندية | `/accounting/documentary-credits` | اعتمادات الاستيراد |
| ٢٥ | خطابات الضمان | `/accounting/guarantees` | خطابات الضمان |
| ٢٦ | التسويات | `/accounting/settlements` | تسويات بين الكيانات |
| ٢٧ | مراكز التكلفة | `/accounting/cost-centers` | توزيع على المراكز |
| ٢٨ | الأبعاد المحاسبية | `/accounting/dimensions` | أبعاد إضافية للتحليل |
| ٢٩ | الموازنات التقديرية | `/accounting/budgets` | الموازنة مقابل الفعلي |
| ٣٠ | الحركات اليومية | `/accounting/daily-transactions` | حركة اليوم |
| ٣١ | حركة النقدية اليومية | `/accounting/statements/daily-cash` | نقدية داخلة وخارجة |
| ٣٢ | **فئات الأصول** | `/inventory/asset-categories` | ربط كل فئة بحساباتها التلاتة |
| ٣٣ | الأصول والإهلاك | `/inventory/assets` | تسجيل الأصول + تشغيل الإهلاك + الاستبعاد |
| ٣٤ | عهدة الأصول | `/inventory/assets/custody` | مين مستلم الأصل |
| ٣٥ | الجرد | `/inventory/audits` | جرد المخزون وتسوية الفروق |
| ٣٦ | أرصدة افتتاحية للمخزون | `/inventory/opening-balances` | أرصدة أول المدة للأصناف |
| ٣٧ | كشوف الرواتب | `/hr/payroll` | حساب وصرف الرواتب |
| ٣٨ | السلف والقروض | `/hr/loans` | سلف الموظفين |
| ٣٩ | نهاية الخدمة | `/hr/end-of-service` | مستحقات ترك الخدمة |
> **دي الحاجات اللي النظام مش هيقدر يقررها لوحده. اقراها قبل الاجتماع.** ---
### 🔴 ١ — سنوات مالية متداخلة
فيه **سنتين ماليتين بيغطوا نفس الأيام**:
| السنة | الفترة | ## ١٢ — حاجات محتاجة قرار منك
|---|---|
| السنة المالية ٢٠٢٤/٢٠٢٥ | ٢٠٢٤-٠٧-٠١ → ٢٠٢٥-٠٦-٣٠ |
| السنة المالية ٢٠٢٤ | ٢٠٢٤-٠١-٠١ → ٢٠٢٤-١٢-٣١ |
| السنة المالية ٢٠٢٥ | ٢٠٢٥-٠١-٠١ → ٢٠٢٥-١٢-٣١ |
**المشكلة:** القيد اللي تاريخه في التداخل تابع للاتنين، وإقفال واحدة بيسيب > دي الحاجات اللي **النظام مش هيقررها لوحده** — بس دلوقتي **لكل واحدة فيهم أداة**.
التانية مفتوحة على نفس العمليات.
**القرار المطلوب:** السنة المالية للنادي **تقويمية (يناير–ديسمبر)** ولا ### 🔴 ١ — سنوات مالية متداخلة
**يوليو–يونيو**؟ بعد ما تقرر، أرشِف السنين التانية.
**الشاشة:** `/accounting/fiscal-years` — التحذير ظاهر فوق. فيه سنتين ماليتين بيغطوا نفس الأيام (تقويمية + يوليو–يونيو).
### 🔴 ٢ — سجل الأصول الثابتة فاضي **الأداة:** `/accounting/fiscal-years` — التحذير فوق وجنب كل سنة زرار **«أرشِف»**.
**القرار:** تقويمية ولا يوليو–يونيو؟
الدفاتر فيها **٤٬٧٠٥٬٦٨٦ آلات ومعدات** و**٤١٬٧٠٣٬٨٦٧ مشروعات تحت التنفيذ**، ### 🔴 ٢ — سجل الأصول الثابتة
وسجل الأصول **فاضي**. يعني **مفيش إهلاك بيتحسب**.
**المطلوب:** سجّل الأصول من `/inventory/assets` واختار **«رصيد افتتاحي»**. الدفاتر فيها أصول ثابتة كبيرة والسجل فاضي → **مفيش إهلاك بيتحسب**.
**ملحوظة على المشروعات تحت التنفيذ:** دي **ما بتتهلكش** وهي تحت التنفيذ. **الأداة:** `/inventory/assets`**«تسجيل أصل ثابت»****«رصيد افتتاحي»**.
أول ما المشروع يخلص، يتحوّل لحساب الأصل المناسب ويبدأ الإهلاك. **للمشروعات اللي خلصت:** اختار **«رسملة مشروع تحت التنفيذ»**.
### 🟠 ٣ — أرصدة على حسابات رئيسية ### 🟠 ٣ — أرصدة على حسابات رئيسية
القيد الافتتاحي القديم نزّل أرصدة على حسابات **رئيسية**: القيد الافتتاحي نزّل أرصدة على حسابات **رئيسية**، فمينفعش drill-down.
| الحساب | الرصيد |
|---|---|
| ١١٠١٠٣ آلات ومعدات (رئيسي) | ٤٬٧٩٩٬٤٣٦ |
| ١١٠٣ مشروعات تحت التنفيذ (رئيسي) | ٤١٬٧٠٣٬٨٦٧ |
| ٢٣٠١٠١ مجمع إهلاك (رئيسي) | ٥٧٩٬٠٤٨ |
**المشكلة:** مينفعش تعمل drill-down، والأرصدة مش موزّعة على الحسابات الفرعية.
**المطلوب:** قيد تسوية ينقل الأرصدة للحسابات الفرعية الصح.
النظام **بيمنع** ده في القيود الجديدة، بس دي أرصدة قديمة.
### 🟠 ٤ — أرقام الحسابات البنكية **الأداة:** `/accounting/reclassification` — بتعرضهم كلهم وبتنقلهم بقيد.
الأربع حسابات البنكية أرقامها **مؤقتة**. عدّلهم من `/accounting/bank-accounts` ### 🟠 ٤ — أرقام الحسابات البنكية مؤقتة
قبل أول إيداع.
### 🟠 ٥ — ٣ مسيرات رواتب محسوبة وما اتصرفتش **الأداة:** `/accounting/bank-accounts` → عدّل كل حساب.
مفيش مصروف رواتب في الدفاتر لأن القيد بيتعمل عند **الصرف**. ### 🟠 ٥ — مسيرات رواتب محسوبة وما اتصرفتش
لو المفروض اتصرفت، اصرفها من `/hr/payroll`.
### 🟡 ٦ — ٩ لاعبين مربوطين بعضو مش موجود القيد بيتعمل عند الصرف. **الأداة:** `/hr/payroll`.
خانة العضو فيها **رقم عضوية** مش رقم العضو. الاستحقاق بيتقيّد بس الدين ### 🟡 ٦ — لاعبين مربوطين بعضو مش موجود
**مش بيظهر على حساب العضو**.
**الشاشة:** `/accounting/gaps` — القائمة ظاهرة مع زرار «صلّح الربط». **الأداة:** `/accounting/gaps` → زرار **«صلّح الربط»**.
### 🟡 ٧ — مفيش ولا فترة اتقفلت ### 🟡 ٧ — مفيش ولا فترة اتقفلت
`period_closings` فاضي — يعني مفيش شهر أو سنة اتقفلت خالص. **الأداة:** `/accounting/period-closing` — ابدأ بالترتيب من أول شهر.
ده كان **بسبب عطل في النظام اتصلّح دلوقتي**. ابدأ الإقفال بالترتيب.
--- ---
...@@ -616,4 +758,4 @@ flowchart TB ...@@ -616,4 +758,4 @@ flowchart TB
--- ---
*آخر تحديث: ٢٠٢٦-٠٩-٠٧ — الأرقام في الدليل من نسخة الدفاتر الحقيقية وقت الكتابة.* *آخر تحديث: ٢٠٢٦-٠٩-٠٧*
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