Commit f4f4b150 authored by DevPilot's avatar DevPilot

docs: دليل PDF للمحاسبة والأصول الثابتة والخزينة (67 شاشة بلقطات حقيقية)

دليل عملي واحد يغطي التلات مجالات المطلوبة بس:
- المحاسبة: الإعداد، دليل الحسابات، القيود، البنوك والشيكات، الاعتمادات
  والضمانات والقروض، وكل التقارير والقوائم المالية.
- الأصول الثابتة: السجل، الإضافة، الصيانة، العهدة، الموردين.
- الخزينة والكاشير: الورديات، التحصيل، التسويات، العهدة، الإيداعات البنكية.

كل شاشة ليها لقطة حقيقية من النظام الحي + شرح إيه اللي بتعمله وإزاي
تستخدمها والقواعد المهمة فيها.

أدوات التوليد اتحفظت تحت tools/pdf عشان الدليل يتبني تاني بعد أي تعديل.
اللقطات بتتفحص إنها مش صفحة تسجيل دخول قبل ما تتقبل — أول محاولة طلعت
65 لقطة من 67 صفحة لوجن من غير ما السكريبت ياخد باله.
parent 4197b68a
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""دليل المحاسبة والأصول والخزينة — لقطات حقيقية من النظام مع شرح الاستخدام."""
import json, os
import arabic_reshaper
from bidi.algorithm import get_display
from PIL import Image
Image.MAX_IMAGE_PIXELS = None
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.lib import colors
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.enums import TA_RIGHT, TA_CENTER
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
PageBreak, HRFlowable, Image as RLImage, KeepTogether)
F = "/tmp/fin_fonts"
pdfmetrics.registerFont(TTFont("AR", f"{F}/Amiri-Regular.ttf"))
pdfmetrics.registerFont(TTFont("AR-B", f"{F}/Amiri-Bold.ttf"))
pdfmetrics.registerFont(TTFont("AR-S", f"{F}/Amiri-Bold.ttf"))
resh = arabic_reshaper.ArabicReshaper()
def ar(t):
"""سطر واحد: تشكيل ثم عكس للعرض."""
return get_display(resh.reshape(t or ""))
def ar_wrap(t, style, width):
"""
النص العربي الطويل لازم يتقسّم لسطور *قبل* ما يتعكس للعرض.
لو عكسنا النص كله الأول وسبنا reportlab يقسّمه، السطور بتطلع
بترتيب مقلوب — آخر سطر فوق وأول سطر تحت.
"""
from reportlab.pdfbase.pdfmetrics import stringWidth
shaped = resh.reshape(t or "")
words = shaped.split(' ')
lines, cur = [], ''
for w in words:
trial = (cur + ' ' + w).strip()
if stringWidth(trial, style.fontName, style.fontSize) <= width or not cur:
cur = trial
else:
lines.append(cur); cur = w
if cur:
lines.append(cur)
return '<br/>'.join(get_display(l) for l in lines)
DARK = colors.HexColor("#111827")
GRAY = colors.HexColor("#6B7280")
LINE = colors.HexColor("#E5E7EB")
S = {
"title": ParagraphStyle("t", fontName="AR-B", fontSize=30, leading=46, textColor=DARK, alignment=TA_CENTER, spaceAfter=10),
"sub": ParagraphStyle("s", fontName="AR", fontSize=14, leading=26, textColor=colors.HexColor("#0D7377"), alignment=TA_CENTER, spaceAfter=6),
"meta": ParagraphStyle("m", fontName="AR", fontSize=10, leading=19, textColor=GRAY, alignment=TA_CENTER),
"band": ParagraphStyle("b", fontName="AR-B", fontSize=18, leading=32, textColor=colors.white, alignment=TA_RIGHT),
"h2": ParagraphStyle("h2", fontName="AR-B", fontSize=13.5, leading=26, textColor=DARK, alignment=TA_RIGHT, spaceAfter=2),
"cap": ParagraphStyle("c", fontName="AR", fontSize=10.5, leading=20, textColor=colors.HexColor("#374151"), alignment=TA_RIGHT, spaceAfter=7),
"path": ParagraphStyle("p", fontName="Helvetica", fontSize=8.5, leading=14, textColor=GRAY, alignment=TA_RIGHT, spaceAfter=6),
"toc_h": ParagraphStyle("th", fontName="AR-B", fontSize=13, leading=28, textColor=DARK, alignment=TA_RIGHT, spaceBefore=10),
"toc": ParagraphStyle("tc", fontName="AR", fontSize=10.5, leading=20, textColor=colors.HexColor("#374151"), alignment=TA_RIGHT, rightIndent=14),
}
MARGIN = 2.0 * cm
W = A4[0] - 2 * MARGIN # عرض الإطار الحقيقي — لازم يطابق هوامش الصفحة بالظبط
def P(t, st="cap"):
style = S[st]
# الأنماط اللي بتتلف على أكتر من سطر لازم تتقسّم قبل العكس
if st in ("cap", "intro", "toc", "step"):
# إطار reportlab بيضيف 6pt حشو من كل ناحية افتراضياً — لازم نخصمها،
# وإلا بيلفّ كلمة زيادة وتظهر لوحدها في سطر تاني بعد العكس
avail = W - style.rightIndent - style.leftIndent - 12 - 4
return Paragraph(ar_wrap(t, style, avail), style)
return Paragraph(ar(t), style)
def band(title, hexcolor):
t = Table([[P(title, "band")]], colWidths=[W])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), colors.HexColor(hexcolor)),
("TOPPADDING", (0,0), (-1,-1), 12), ("BOTTOMPADDING", (0,0), (-1,-1), 12),
("LEFTPADDING", (0,0), (-1,-1), 16), ("RIGHTPADDING", (0,0), (-1,-1), 16)]))
return t
def shot(path, maxh=21.0*cm):
im = Image.open(path).convert('RGB')
w, h = im.size
# اقصص الطول الزائد عشان الصورة تفضل مقروءة
max_ratio = 1.28 # أطول من كده الصورة بتتصغّر وتبقى مش مقروءة
if h / w > max_ratio:
im = im.crop((0, 0, w, int(w * max_ratio)))
w, h = im.size
# صغّر واحفظ JPEG — الـ PNG بدقة 2x بيطلع ملف ضخم من غير فايدة في الطباعة
target_w = 1700
if w > target_w:
im = im.resize((target_w, int(h * target_w / w)), Image.LANCZOS)
w, h = im.size
path = path.replace('.png', '_o.jpg')
im.save(path, 'JPEG', quality=72, optimize=True)
dw = W
dh = dw * h / w
if dh > maxh:
dh = maxh; dw = dh * w / h
img = RLImage(path, width=dw, height=dh)
t = Table([[img]], colWidths=[dw])
t.setStyle(TableStyle([("BOX", (0,0), (-1,-1), 0.7, LINE),
("TOPPADDING", (0,0), (-1,-1), 0), ("BOTTOMPADDING", (0,0), (-1,-1), 0),
("LEFTPADDING", (0,0), (-1,-1), 0), ("RIGHTPADDING", (0,0), (-1,-1), 0)]))
return t
def footer(canvas, doc):
canvas.saveState()
canvas.setFont("AR", 8)
canvas.setFillColor(GRAY)
canvas.drawCentredString(A4[0] / 2, 1.05 * cm, str(doc.page))
canvas.setFont("AR", 7.5)
canvas.drawRightString(A4[0] - 2.0 * cm, 1.05 * cm, ar("دليل المحاسبة والأصول والخزينة"))
canvas.restoreState()
def main():
man = json.load(open('/tmp/pw_build/fin_manifest.json', encoding='utf-8'))
man = [m for m in man if m.get('file')]
out = '/config/workspace/projects/clubphp/docs/دليل_المحاسبة_والأصول_والخزينة.pdf'
os.makedirs(os.path.dirname(out), exist_ok=True)
doc = SimpleDocTemplate(out, pagesize=A4,
rightMargin=MARGIN, leftMargin=MARGIN,
topMargin=1.8*cm, bottomMargin=1.8*cm,
title="دليل المحاسبة والأصول والخزينة")
st = []
# ── الغلاف ───────────────────────────────────────────────────────────
st += [Spacer(1, 5.5*cm), P("دليل المحاسبة والأصول والخزينة", "title"),
P("شرح عملي لكل شاشة بلقطات حقيقية من النظام", "sub"),
Spacer(1, 0.5*cm),
HRFlowable(width="45%", thickness=1, color=LINE, hAlign="CENTER"),
Spacer(1, 0.5*cm),
P("نظام إدارة النادي", "meta"),
P("المحاسبة العامة · الأصول الثابتة · الخزينة والكاشير", "meta"),
PageBreak()]
# ── الفهرس ───────────────────────────────────────────────────────────
st += [P("المحتويات", "h2"), Spacer(1, 0.25*cm)]
seen = []
for m in man:
if m['sec'] not in seen:
seen.append(m['sec'])
st.append(P(m['sec'], "toc_h"))
st.append(P("• " + m['l'], "toc"))
st.append(PageBreak())
# ── الشاشات ──────────────────────────────────────────────────────────
cur = None
for m in man:
if m['sec'] != cur:
if cur is not None:
st.append(PageBreak())
cur = m['sec']
st += [band(cur, m['color']), Spacer(1, 0.45*cm)]
blk = [P(m['l'], "h2"), Paragraph(m['p'], S['path']), P(m['c'], "cap"),
shot(f"/tmp/fin_shots/{m['file']}"), Spacer(1, 0.75*cm)]
st.append(KeepTogether(blk))
doc.build(st, onFirstPage=lambda c, d: None, onLaterPages=footer)
size = os.path.getsize(out) / 1024 / 1024
print(f"{out}\n{len(man)} screens, {size:.1f} MB")
if __name__ == '__main__':
main()
const { chromium } = require('playwright');
const fs = require('fs');
const BASE = 'https://clubmanagement.caprover.al-arcade.com';
const screens = JSON.parse(fs.readFileSync('/tmp/pw_build/fin_screens.json', 'utf8'));
const OUT = '/tmp/fin_shots';
if (!fs.existsSync(OUT)) fs.mkdirSync(OUT, { recursive: true });
async function login(page) {
await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded', timeout: 40000 });
await page.fill('input[name=username], input[type=text]', 'admin');
await page.fill('input[name=password], input[type=password]', 'Alarcade123#');
await page.click('button[type=submit]');
await page.waitForLoadState('domcontentloaded', { timeout: 40000 });
return !page.url().includes('/login');
}
// A screen counts as captured ONLY if we are genuinely logged in on it.
async function isLoginPage(page) {
if (page.url().includes('/login')) return true;
const n = await page.locator('input[name=password], input[type=password]').count();
if (n > 0 && (await page.locator('form').count()) > 0) {
const body = await page.textContent('body').catch(() => '');
if (/تسجيل الدخول|اسم المستخدم/.test(body)) return true;
}
return false;
}
(async () => {
const browser = await chromium.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'] });
const ctx = await browser.newContext({ viewport: { width: 1500, height: 950 }, deviceScaleFactor: 2 });
const page = await ctx.newPage();
// شريط الـ debug مالوش لازمة في دليل تعليمي
await ctx.addInitScript(() => {
const css = `#debug_toolbar,.debug-toolbar,[id*="debug-bar"],[class*="debug-bar"],
#debugbar,.debugbar{display:none !important;}`;
document.addEventListener('DOMContentLoaded', () => {
const st = document.createElement('style'); st.textContent = css;
document.head.appendChild(st);
});
});
if (!(await login(page))) { console.log('LOGIN FAILED'); process.exit(1); }
console.log('login ok\n');
const manifest = [];
for (let i = 0; i < screens.length; i++) {
const s = screens[i];
const file = `${String(i + 1).padStart(3, '0')}.png`;
let ok = false, note = '';
for (let attempt = 1; attempt <= 3 && !ok; attempt++) {
try {
const resp = await page.goto(BASE + s.p, { waitUntil: 'networkidle', timeout: 40000 });
await page.waitForTimeout(600);
if (await isLoginPage(page)) {
note = `logged out (attempt ${attempt}) — re-login`;
await page.waitForTimeout(2500);
if (!(await login(page))) { await page.waitForTimeout(8000); await login(page); }
continue; // retry this screen
}
const body = await page.textContent('body').catch(() => '');
if (/Exception:|View not found|Fatal error/i.test(body)) {
manifest.push({ ...s, file: null, broken: true, note: 'PHP error' });
console.log(`BROKEN ${s.p}`);
ok = true; break;
}
await page.evaluate(() => {
document.querySelectorAll('*').forEach(el => {
const t = (el.textContent || '').trim();
if (/^DEBUG\b/.test(t) && t.length < 200 && el.children.length < 12) el.style.display = 'none';
});
}).catch(() => {});
await page.screenshot({ path: `${OUT}/${file}`, fullPage: true });
manifest.push({ ...s, file, broken: false, status: resp ? resp.status() : 0 });
console.log(` ok ${s.p}${note ? ' (' + note + ')' : ''}`);
ok = true;
} catch (e) {
note = String(e).slice(0, 70);
await page.waitForTimeout(3000);
}
}
if (!ok) {
manifest.push({ ...s, file: null, broken: true, note });
console.log(` FAIL ${s.p}${note}`);
}
}
fs.writeFileSync('/tmp/pw_build/fin_manifest.json', JSON.stringify(manifest, null, 1));
const good = manifest.filter(m => m.file);
console.log(`\ncaptured ${good.length}/${screens.length}`);
manifest.filter(m => !m.file).forEach(b => console.log(' MISSING: ' + b.p + ' — ' + (b.note || '')));
await browser.close();
process.exit(good.length === screens.length ? 0 : 1);
})();
This diff is collapsed.
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