Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions treasury_app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# نظام إدارة الخزينة والعهدة
## Treasury & Expense Management System

برنامج ويب مستقل لإدارة الخزينة والعهدة ومطالبات السداد.

### المتطلبات
- Python 3.10+

### التثبيت والتشغيل

```bash
# 1. تثبيت المكتبات
pip install -r requirements.txt

# 2. تشغيل البرنامج
python run.py

# 3. افتح المتصفح
# http://localhost:5000
```

### المميزات
- ✅ إدارة الخزائن (نقدي / بنكي)
- ✅ إدارة العهد مع دورة كاملة
- ✅ مطالبات سداد الموردين
- ✅ مطالبات المصاريف
- ✅ سندات الصرف والقبض
- ✅ قيود محاسبية تلقائية
- ✅ تقارير شاملة
- ✅ طباعة السندات
- ✅ واجهة عربية RTL
75 changes: 75 additions & 0 deletions treasury_app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import os
from flask import Flask, render_template, redirect, url_for, flash, request, jsonify
from models import db, Treasury, Custody, PaymentClaim, Payment, Sequence
from routes.treasury import treasury_bp
from routes.custody import custody_bp
from routes.claims import claims_bp
from routes.payments import payments_bp
from routes.reports import reports_bp


def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = 'treasury-secret-key-2024'
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.path.dirname(__file__), 'treasury.db')}"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db.init_app(app)

app.register_blueprint(treasury_bp, url_prefix='/treasury')
app.register_blueprint(custody_bp, url_prefix='/custody')
app.register_blueprint(claims_bp, url_prefix='/claims')
app.register_blueprint(payments_bp, url_prefix='/payments')
app.register_blueprint(reports_bp, url_prefix='/reports')

with app.app_context():
db.create_all()

@app.route('/')
def dashboard():
treasuries = Treasury.query.filter_by(state='open').all()
pending_claims = PaymentClaim.query.filter(
PaymentClaim.state.in_(['submitted', 'approved', 'partial'])
).count()
pending_custodies = Custody.query.filter(
Custody.state.in_(['confirmed', 'paid'])
).count()
recent_payments = Payment.query.filter_by(state='done').order_by(Payment.date.desc()).limit(10).all()
total_balance = sum(t.balance for t in treasuries)
stats = {
'total_balance': total_balance,
'pending_claims': pending_claims,
'pending_custodies': pending_custodies,
'treasuries_count': len(treasuries),
}
return render_template('dashboard.html',
stats=stats,
treasuries=treasuries,
recent_payments=recent_payments)

@app.template_filter('currency')
def currency_filter(value, symbol='ر.س'):
if value is None:
return f'0.00 {symbol}'
return f'{value:,.2f} {symbol}'

@app.template_filter('ar_date')
def ar_date_filter(value):
if not value:
return ''
if hasattr(value, 'strftime'):
return value.strftime('%Y/%m/%d')
return str(value)

return app


if __name__ == '__main__':
app = create_app()
print("=" * 50)
print(" نظام إدارة الخزينة والعهدة")
print(" Treasury & Expense Management System")
print("=" * 50)
print(" افتح المتصفح على: http://localhost:5000")
print("=" * 50)
app.run(debug=True, host='0.0.0.0', port=5000)
253 changes: 253 additions & 0 deletions treasury_app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
from datetime import datetime, date
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()


class Treasury(db.Model):
__tablename__ = 'treasuries'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False)
code = db.Column(db.String(50), nullable=False, unique=True)
treasury_type = db.Column(db.String(20), default='cash') # cash / bank
currency = db.Column(db.String(10), default='SAR')
responsible = db.Column(db.String(200))
max_amount = db.Column(db.Float, default=0)
notes = db.Column(db.Text)
state = db.Column(db.String(20), default='open') # open / closed
created_at = db.Column(db.DateTime, default=datetime.now)

payments = db.relationship('Payment', backref='treasury', lazy=True)
custodies = db.relationship('Custody', backref='treasury', lazy=True)
transactions = db.relationship('TreasuryTransaction', backref='treasury', lazy=True, order_by='TreasuryTransaction.date.desc()')

@property
def balance(self):
inflow = sum(p.amount for p in self.payments if p.state == 'done' and p.payment_type == 'inbound')
outflow = sum(p.amount for p in self.payments if p.state == 'done' and p.payment_type == 'outbound')
return inflow - outflow

@property
def type_label(self):
return 'نقدي' if self.treasury_type == 'cash' else 'بنكي'

@property
def state_label(self):
return 'مفتوح' if self.state == 'open' else 'مغلق'


class TreasuryTransaction(db.Model):
__tablename__ = 'treasury_transactions'
id = db.Column(db.Integer, primary_key=True)
treasury_id = db.Column(db.Integer, db.ForeignKey('treasuries.id'), nullable=False)
date = db.Column(db.Date, default=date.today)
description = db.Column(db.String(500), nullable=False)
reference = db.Column(db.String(100))
transaction_type = db.Column(db.String(10)) # in / out
amount = db.Column(db.Float, nullable=False)
balance_after = db.Column(db.Float)
created_at = db.Column(db.DateTime, default=datetime.now)


class Custody(db.Model):
__tablename__ = 'custodies'
id = db.Column(db.Integer, primary_key=True)
number = db.Column(db.String(50), unique=True)
employee = db.Column(db.String(200), nullable=False)
department = db.Column(db.String(200))
treasury_id = db.Column(db.Integer, db.ForeignKey('treasuries.id'), nullable=False)
date = db.Column(db.Date, default=date.today)
due_date = db.Column(db.Date)
amount = db.Column(db.Float, nullable=False)
purpose = db.Column(db.String(500), nullable=False)
notes = db.Column(db.Text)
state = db.Column(db.String(20), default='draft') # draft/confirmed/paid/cleared/cancelled
payment_id = db.Column(db.Integer, db.ForeignKey('payments.id'), nullable=True)
clearance_date = db.Column(db.Date)
cleared_by = db.Column(db.String(200))
created_at = db.Column(db.DateTime, default=datetime.now)

expenses = db.relationship('CustodyExpense', backref='custody', lazy=True, cascade='all,delete-orphan')

@property
def total_expenses(self):
return sum(e.amount for e in self.expenses)

@property
def remaining(self):
return self.amount - self.total_expenses

@property
def state_label(self):
labels = {'draft': 'مسودة', 'confirmed': 'معتمد', 'paid': 'مصروف', 'cleared': 'مسوّى', 'cancelled': 'ملغي'}
return labels.get(self.state, self.state)

@property
def state_color(self):
colors = {'draft': 'secondary', 'confirmed': 'primary', 'paid': 'warning', 'cleared': 'success', 'cancelled': 'danger'}
return colors.get(self.state, 'secondary')


class CustodyExpense(db.Model):
__tablename__ = 'custody_expenses'
id = db.Column(db.Integer, primary_key=True)
custody_id = db.Column(db.Integer, db.ForeignKey('custodies.id'), nullable=False)
date = db.Column(db.Date, default=date.today)
description = db.Column(db.String(500), nullable=False)
account = db.Column(db.String(200))
amount = db.Column(db.Float, nullable=False)
notes = db.Column(db.String(500))


class PaymentClaim(db.Model):
__tablename__ = 'payment_claims'
id = db.Column(db.Integer, primary_key=True)
number = db.Column(db.String(50), unique=True)
claim_type = db.Column(db.String(20), nullable=False) # vendor / expense
date = db.Column(db.Date, default=date.today)
due_date = db.Column(db.Date)
vendor = db.Column(db.String(300))
vendor_ref = db.Column(db.String(200))
employee = db.Column(db.String(200))
department = db.Column(db.String(200))
description = db.Column(db.Text, nullable=False)
notes = db.Column(db.Text)
priority = db.Column(db.String(10), default='normal') # normal/high/urgent
state = db.Column(db.String(20), default='draft') # draft/submitted/approved/partial/paid/rejected/cancelled
approved_by = db.Column(db.String(200))
approved_date = db.Column(db.Date)
rejected_reason = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.now)

lines = db.relationship('PaymentClaimLine', backref='claim', lazy=True, cascade='all,delete-orphan')
payments = db.relationship('Payment', backref='claim', lazy=True)

@property
def subtotal(self):
return sum(l.subtotal for l in self.lines)

@property
def total_amount(self):
return sum(l.subtotal for l in self.lines)

@property
def paid_amount(self):
return sum(p.amount for p in self.payments if p.state == 'done')

@property
def residual(self):
return self.total_amount - self.paid_amount

@property
def state_label(self):
labels = {
'draft': 'مسودة', 'submitted': 'مقدمة للاعتماد',
'approved': 'معتمدة', 'partial': 'مدفوعة جزئياً',
'paid': 'مدفوعة', 'rejected': 'مرفوضة', 'cancelled': 'ملغية'
}
return labels.get(self.state, self.state)

@property
def state_color(self):
colors = {
'draft': 'secondary', 'submitted': 'info', 'approved': 'primary',
'partial': 'warning', 'paid': 'success', 'rejected': 'danger', 'cancelled': 'dark'
}
return colors.get(self.state, 'secondary')

@property
def type_label(self):
return 'مطالبة مورد' if self.claim_type == 'vendor' else 'مطالبة مصاريف'

@property
def priority_label(self):
labels = {'normal': 'عادي', 'high': 'مهم', 'urgent': 'عاجل'}
return labels.get(self.priority, 'عادي')

@property
def priority_color(self):
colors = {'normal': 'secondary', 'high': 'warning', 'urgent': 'danger'}
return colors.get(self.priority, 'secondary')


class PaymentClaimLine(db.Model):
__tablename__ = 'payment_claim_lines'
id = db.Column(db.Integer, primary_key=True)
claim_id = db.Column(db.Integer, db.ForeignKey('payment_claims.id'), nullable=False)
description = db.Column(db.String(500), nullable=False)
account = db.Column(db.String(200))
quantity = db.Column(db.Float, default=1.0)
unit_price = db.Column(db.Float, nullable=False)
notes = db.Column(db.String(500))

@property
def subtotal(self):
return self.quantity * self.unit_price


class Payment(db.Model):
__tablename__ = 'payments'
id = db.Column(db.Integer, primary_key=True)
number = db.Column(db.String(50), unique=True)
payment_type = db.Column(db.String(20), nullable=False) # outbound / inbound
claim_id = db.Column(db.Integer, db.ForeignKey('payment_claims.id'), nullable=True)
custody_id = db.Column(db.Integer, db.ForeignKey('custodies.id'), nullable=True)
date = db.Column(db.Date, default=date.today)
treasury_id = db.Column(db.Integer, db.ForeignKey('treasuries.id'), nullable=False)
beneficiary = db.Column(db.String(300))
amount = db.Column(db.Float, nullable=False)
description = db.Column(db.Text, nullable=False)
payment_method = db.Column(db.String(20), default='cash') # cash/check/transfer
check_number = db.Column(db.String(100))
check_date = db.Column(db.Date)
transfer_ref = db.Column(db.String(200))
reference = db.Column(db.String(200))
notes = db.Column(db.Text)
state = db.Column(db.String(20), default='draft') # draft/confirmed/done/cancelled
confirmed_by = db.Column(db.String(200))
confirmed_at = db.Column(db.DateTime)
done_by = db.Column(db.String(200))
done_at = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.now)

custody_ref = db.relationship('Custody', foreign_keys=[custody_id], backref='payment_voucher', lazy=True)

@property
def state_label(self):
labels = {'draft': 'مسودة', 'confirmed': 'معتمد', 'done': 'منفذ', 'cancelled': 'ملغي'}
return labels.get(self.state, self.state)

@property
def state_color(self):
colors = {'draft': 'secondary', 'confirmed': 'warning', 'done': 'success', 'cancelled': 'danger'}
return colors.get(self.state, 'secondary')

@property
def type_label(self):
return 'سند صرف' if self.payment_type == 'outbound' else 'سند قبض'

@property
def method_label(self):
labels = {'cash': 'نقدي', 'check': 'شيك', 'transfer': 'تحويل بنكي'}
return labels.get(self.payment_method, self.payment_method)


# Sequence helper
class Sequence(db.Model):
__tablename__ = 'sequences'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
prefix = db.Column(db.String(20))
next_number = db.Column(db.Integer, default=1)

@classmethod
def next(cls, name, prefix=''):
seq = cls.query.filter_by(name=name).first()
if not seq:
seq = cls(name=name, prefix=prefix, next_number=1)
db.session.add(seq)
year = datetime.now().year
number = f"{prefix}/{year}/{seq.next_number:04d}"
seq.next_number += 1
db.session.commit()
return number
7 changes: 7 additions & 0 deletions treasury_app/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Flask==3.0.3
Flask-SQLAlchemy==3.1.1
Flask-WTF==1.2.1
WTForms==3.1.2
reportlab==4.2.2
Pillow==10.3.0
python-dateutil==2.9.0
Empty file added treasury_app/routes/__init__.py
Empty file.
Loading