Skip to content

app.py #43

Description

@Aster2012

import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3
import webbrowser
import urllib.parse
from datetime import datetime
import os

class App:
def init(self, root):
self.root = root
self.root.title(" إدارة ديون الزبائن")
self.root.geometry("1200x750")

    # إنشاء قاعدة البيانات والجداول
    self.init_db()
    
    # تنسيق الخطوط والألوان
    style = ttk.Style()
    style.configure("Treeview.Heading", font=("Arial", 11, "bold"))
    style.configure("Treeview", font=("Arial", 11), rowheight=30)
    
    # الإطارات الرئيسية
    self.top_frame = tk.Frame(self.root, bg="#f0f0f0", pady=15, relief=tk.RIDGE, bd=2)
    self.top_frame.pack(fill=tk.X, padx=10, pady=5)
    
    self.main_frame = tk.Frame(self.root)
    self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
    
    # إطار إضافة زبون (اليمين)
    self.right_frame = tk.Frame(self.main_frame, width=350, bd=1, relief=tk.SOLID, padx=15, pady=15)
    self.right_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=5)
    
    # إطار القائمة والبحث (اليسار)
    self.left_frame = tk.Frame(self.main_frame)
    self.left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)
    
    self.setup_top_stats()
    self.setup_add_customer()
    self.setup_customer_list()
    
    # تحميل البيانات عند التشغيل
    self.load_data()

def init_db(self):
    self.conn = sqlite3.connect('accounting_system.db')
    self.c = self.conn.cursor()
    
    # جدول الزبائن
    self.c.execute('''CREATE TABLE IF NOT EXISTS customers (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT,
                        phone TEXT,
                        item TEXT,
                        total_price REAL,
                        paid REAL,
                        remaining REAL,
                        date TEXT
                    )''')
                    
    # جدول حركات الدفع (سجل الزبون)
    self.c.execute('''CREATE TABLE IF NOT EXISTS payments (
                        payment_id INTEGER PRIMARY KEY AUTOINCREMENT,
                        customer_id INTEGER,
                        amount REAL,
                        payment_date TEXT,
                        FOREIGN KEY(customer_id) REFERENCES customers(id)
                    )''')
    self.conn.commit()

def setup_top_stats(self):
    self.lbl_total_customers = tk.Label(self.top_frame, text="مجموع الزبائن: 0", font=("Arial", 14, "bold"), fg="#1976d2", bg="#f0f0f0")
    self.lbl_total_customers.pack(side=tk.RIGHT, expand=True)
    
    self.lbl_total_sales = tk.Label(self.top_frame, text="مجموع سعر البيع: 0", font=("Arial", 14, "bold"), fg="#7b1fa2", bg="#f0f0f0")
    self.lbl_total_sales.pack(side=tk.RIGHT, expand=True)
    
    self.lbl_total_debt = tk.Label(self.top_frame, text="مجموع الديون: 0", font=("Arial", 14, "bold"), fg="#d32f2f", bg="#f0f0f0")
    self.lbl_total_debt.pack(side=tk.RIGHT, expand=True)
    
    self.lbl_total_received = tk.Label(self.top_frame, text="مجموع الواصل: 0", font=("Arial", 14, "bold"), fg="#388e3c", bg="#f0f0f0")
    self.lbl_total_received.pack(side=tk.RIGHT, expand=True)

def setup_add_customer(self):
    tk.Label(self.right_frame, text="إضافة زبون جديد", font=("Arial", 16, "bold"), fg="#1e88e5").pack(pady=10)
    
    def create_input(label_text):
        tk.Label(self.right_frame, text=label_text, font=("Arial", 12)).pack(anchor=tk.E, pady=(5,0))
        ent = tk.Entry(self.right_frame, font=("Arial", 12), justify='right', bd=2, relief=tk.GROOVE)
        ent.pack(fill=tk.X, pady=5)
        return ent

    self.ent_name = create_input("اسم الزبون الكامل:")
    self.ent_phone = create_input("رقم الهاتف:")
    self.ent_item = create_input("اسم المادة:")
    self.ent_price = create_input("سعر البيع الكلي:")
    self.ent_paid = create_input("المبلغ الواصل (مقدمة):")
    
    btn_save = tk.Button(self.right_frame, text="حفظ بيانات الزبون", bg="#2e7d32", fg="white", font=("Arial", 13, "bold"), command=self.save_customer)
    btn_save.pack(fill=tk.X, pady=25)

def setup_customer_list(self):
    # شريط البحث
    search_frame = tk.Frame(self.left_frame)
    search_frame.pack(fill=tk.X, pady=5)
    
    self.ent_search = tk.Entry(search_frame, font=("Arial", 12), justify='right')
    self.ent_search.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=5)
    self.ent_search.bind("<KeyRelease>", self.search_customer)
    tk.Label(search_frame, text="بحث بالاسم:", font=("Arial", 12, "bold")).pack(side=tk.RIGHT)
    
    # جدول عرض البيانات
    columns = ("date", "remaining", "paid", "price", "item", "phone", "name", "id")
    self.tree = ttk.Treeview(self.left_frame, columns=columns, show="headings")
    
    self.tree.heading("id", text="ID")
    self.tree.heading("name", text="اسم الزبون")
    self.tree.heading("phone", text="رقم الهاتف")
    self.tree.heading("item", text="المادة")
    self.tree.heading("price", text="السعر الكلي")
    self.tree.heading("paid", text="الواصل")
    self.tree.heading("remaining", text="المتبقي (الديون)")
    self.tree.heading("date", text="تاريخ الإضافة")
    
    self.tree.column("id", width=40, anchor=tk.CENTER)
    self.tree.column("name", width=160, anchor=tk.E)
    self.tree.column("phone", width=120, anchor=tk.CENTER)
    self.tree.column("item", width=120, anchor=tk.E)
    self.tree.column("price", width=90, anchor=tk.CENTER)
    self.tree.column("paid", width=90, anchor=tk.CENTER)
    self.tree.column("remaining", width=90, anchor=tk.CENTER)
    self.tree.column("date", width=100, anchor=tk.CENTER)
    
    self.tree.pack(fill=tk.BOTH, expand=True, pady=10)
    
    # أزرار الإجراءات السفلية
    btn_frame = tk.Frame(self.left_frame)
    btn_frame.pack(fill=tk.X, pady=5)
    
    # صف الأزرار الأول
    top_btn_row = tk.Frame(btn_frame)
    top_btn_row.pack(fill=tk.X, pady=5)
    
    tk.Button(top_btn_row, text="سجل الدفعات والاستقطاع 💰", bg="#ff9800", fg="white", font=("Arial", 12, "bold"), command=self.open_history_window).pack(side=tk.RIGHT, padx=5)
    tk.Button(top_btn_row, text="تعديل بيانات الزبون ✏️", bg="#0288d1", fg="white", font=("Arial", 12, "bold"), command=self.edit_customer_dialog).pack(side=tk.RIGHT, padx=5)
    
    # صف الأزرار الثاني
    bottom_btn_row = tk.Frame(btn_frame)
    bottom_btn_row.pack(fill=tk.X, pady=5)
    
    tk.Button(bottom_btn_row, text="إرسال واتساب 💬", bg="#25d366", fg="white", font=("Arial", 11, "bold"), command=self.send_whatsapp).pack(side=tk.LEFT, padx=5)
    tk.Button(bottom_btn_row, text="طباعة كشف 🖨️", bg="#455a64", fg="white", font=("Arial", 11, "bold"), command=self.print_info).pack(side=tk.LEFT, padx=5)
    tk.Button(bottom_btn_row, text="حذف الزبون 🗑️", bg="#d32f2f", fg="white", font=("Arial", 11, "bold"), command=self.delete_customer).pack(side=tk.RIGHT, padx=5)

def save_customer(self):
    name = self.ent_name.get()
    phone = self.ent_phone.get()
    item = self.ent_item.get()
    price_str = self.ent_price.get()
    paid_str = self.ent_paid.get()
    
    if not name or not price_str:
        messagebox.showerror("نقص بالبيانات", "يرجى إدخال اسم الزبون وسعر البيع على الأقل.")
        return
        
    try:
        price = float(price_str)
        paid = float(paid_str) if paid_str else 0.0
        remaining = price - paid
        date_now = datetime.now().strftime("%Y-%m-%d %H:%M")
        
        self.c.execute("INSERT INTO customers (name, phone, item, total_price, paid, remaining, date) VALUES (?, ?, ?, ?, ?, ?, ?)",
                       (name, phone, item, price, paid, remaining, date_now))
        customer_id = self.c.lastrowid
        
        # إذا كان هناك مبلغ مدفوع مبدئياً، نحفظه كأول دفعة في السجل
        if paid > 0:
            self.c.execute("INSERT INTO payments (customer_id, amount, payment_date) VALUES (?, ?, ?)",
                           (customer_id, paid, date_now))
            
        self.conn.commit()
        
        self.clear_entries()
        self.load_data()
        messagebox.showinfo("نجاح", "تم حفظ معلومات الزبون بنجاح!")
    except ValueError:
        messagebox.showerror("خطأ في الإدخال", "يرجى التأكد من كتابة الأرقام بشكل صحيح.")

def clear_entries(self):
    self.ent_name.delete(0, tk.END)
    self.ent_phone.delete(0, tk.END)
    self.ent_item.delete(0, tk.END)
    self.ent_price.delete(0, tk.END)
    self.ent_paid.delete(0, tk.END)

def load_data(self, search_query=""):
    for row in self.tree.get_children():
        self.tree.delete(row)
        
    if search_query:
        self.c.execute("SELECT * FROM customers WHERE name LIKE ?", ('%' + search_query + '%',))
    else:
        self.c.execute("SELECT * FROM customers")
        
    rows = self.c.fetchall()
    
    total_customers = len(rows)
    total_sales = sum(row[4] for row in rows)
    total_paid = sum(row[5] for row in rows)
    total_debt = sum(row[6] for row in rows)
    
    self.lbl_total_customers.config(text=f"مجموع الزبائن: {total_customers}")
    self.lbl_total_sales.config(text=f"مجموع سعر البيع: {total_sales:,.0f}")
    self.lbl_total_received.config(text=f"مجموع الواصل: {total_paid:,.0f}")
    self.lbl_total_debt.config(text=f"مجموع الديون: {total_debt:,.0f}")
    
    for row in rows:
        self.tree.insert("", tk.END, values=(row[7], f"{row[6]:,.0f}", f"{row[5]:,.0f}", f"{row[4]:,.0f}", row[3], row[2], row[1], row[0]))

def search_customer(self, event):
    query = self.ent_search.get()
    self.load_data(query)

# ================= الميزات الجديدة ================= #

def edit_customer_dialog(self):
    selected = self.tree.focus()
    if not selected:
        messagebox.showwarning("تنبيه", "يرجى تحديد زبون من القائمة لتعديل بياناته.")
        return
        
    values = self.tree.item(selected, 'values')
    # values = (date, remaining, paid, price, item, phone, name, id)
    customer_id = values[7]
    
    # استرجاع البيانات الأصلية من قاعدة البيانات
    self.c.execute("SELECT name, phone, item, total_price FROM customers WHERE id=?", (customer_id,))
    cust_data = self.c.fetchone()
    
    edit_win = tk.Toplevel(self.root)
    edit_win.title("تعديل بيانات الزبون")
    edit_win.geometry("400x350")
    edit_win.grab_set() # لمنع النقر خارج النافذة
    
    tk.Label(edit_win, text=f"تعديل بيانات: {cust_data[0]}", font=("Arial", 14, "bold")).pack(pady=10)
    
    def create_edit_input(label_text, default_val):
        tk.Label(edit_win, text=label_text, font=("Arial", 12)).pack(anchor=tk.E, padx=20)
        ent = tk.Entry(edit_win, font=("Arial", 12), justify='right')
        ent.insert(0, str(default_val))
        ent.pack(fill=tk.X, padx=20, pady=5)
        return ent

    ent_new_name = create_edit_input("الاسم:", cust_data[0])
    ent_new_phone = create_edit_input("الهاتف:", cust_data[1])
    ent_new_item = create_edit_input("المادة:", cust_data[2])
    ent_new_price = create_edit_input("السعر الكلي الجديد:", cust_data[3])
    
    def save_edits():
        try:
            new_price = float(ent_new_price.get())
            # إعادة حساب الديون المتبقية بناء على السعر الجديد
            self.c.execute("SELECT paid FROM customers WHERE id=?", (customer_id,))
            current_paid = self.c.fetchone()[0]
            new_remaining = new_price - current_paid
            
            self.c.execute('''UPDATE customers 
                              SET name=?, phone=?, item=?, total_price=?, remaining=? 
                              WHERE id=?''', 
                           (ent_new_name.get(), ent_new_phone.get(), ent_new_item.get(), new_price, new_remaining, customer_id))
            self.conn.commit()
            self.load_data()
            edit_win.destroy()
            messagebox.showinfo("نجاح", "تم تعديل بيانات الزبون بنجاح!")
        except ValueError:
            messagebox.showerror("خطأ", "السعر يجب أن يكون رقماً.")
            
    tk.Button(edit_win, text="حفظ التعديلات", bg="#1976d2", fg="white", font=("Arial", 12, "bold"), command=save_edits).pack(pady=15, fill=tk.X, padx=20)

def open_history_window(self):
    selected = self.tree.focus()
    if not selected:
        messagebox.showwarning("تنبيه", "يرجى تحديد زبون لعرض سجل دفعاته.")
        return
        
    values = self.tree.item(selected, 'values')
    customer_id = values[7]
    customer_name = values[6]
    
    hist_win = tk.Toplevel(self.root)
    hist_win.title(f"سجل حركات ودفعات الزبون - {customer_name}")
    hist_win.geometry("600x600")
    hist_win.grab_set()
    
    # معلومات الحساب العلوية
    info_frame = tk.Frame(hist_win, bg="#e3f2fd", pady=10, relief=tk.SOLID, bd=1)
    info_frame.pack(fill=tk.X, padx=10, pady=10)
    
    lbl_info = tk.Label(info_frame, text="", font=("Arial", 13, "bold"), bg="#e3f2fd", justify="right")
    lbl_info.pack()

    # إطار الاستقطاع (الدفع)
    pay_frame = tk.Frame(hist_win, pady=10)
    pay_frame.pack(fill=tk.X, padx=10)
    
    tk.Label(pay_frame, text="استقطاع / تسديد مبلغ جديد:", font=("Arial", 12, "bold"), fg="#d32f2f").pack(side=tk.RIGHT)
    ent_payment = tk.Entry(pay_frame, font=("Arial", 14), justify='center', width=15)
    ent_payment.pack(side=tk.RIGHT, padx=10)
    
    # جدول الحركات
    tree_hist = ttk.Treeview(hist_win, columns=("date", "amount"), show="headings", height=12)
    tree_hist.heading("amount", text="المبلغ المدفوع (الاستقطاع)")
    tree_hist.heading("date", text="تاريخ ووقت الحركة")
    tree_hist.column("amount", anchor=tk.CENTER)
    tree_hist.column("date", anchor=tk.CENTER)
    tree_hist.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

    # دوال التحديث المدمجة بالنافذة
    def refresh_history():
        # جلب تفاصيل الزبون المحدثة
        self.c.execute("SELECT total_price, paid, remaining FROM customers WHERE id=?", (customer_id,))
        price, paid, rem = self.c.fetchone()
        lbl_info.config(text=f"الزبون: {customer_name} | السعر الكلي: {price:,.0f} | الواصل: {paid:,.0f} | المتبقي (الديون): {rem:,.0f} د.ع")
        
        # جلب الحركات
        for row in tree_hist.get_children(): tree_hist.delete(row)
        self.c.execute("SELECT amount, payment_date FROM payments WHERE customer_id=? ORDER BY payment_id DESC", (customer_id,))
        for r in self.c.fetchall():
            tree_hist.insert("", tk.END, values=(r[1], f"{r[0]:,.0f} د.ع"))
            
    def make_payment():
        amount_str = ent_payment.get()
        if not amount_str: return
        try:
            payment_amount = float(amount_str)
            if payment_amount <= 0: return
            
            # جلب البيانات الحالية
            self.c.execute("SELECT total_price, paid FROM customers WHERE id=?", (customer_id,))
            price, current_paid = self.c.fetchone()
            
            new_paid = current_paid + payment_amount
            new_remaining = price - new_paid
            date_now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            
            # تحديث جدول الزبائن وجدول الحركات
            self.c.execute("UPDATE customers SET paid=?, remaining=? WHERE id=?", (new_paid, new_remaining, customer_id))
            self.c.execute("INSERT INTO payments (customer_id, amount, payment_date) VALUES (?, ?, ?)", (customer_id, payment_amount, date_now))
            self.conn.commit()
            
            ent_payment.delete(0, tk.END)
            refresh_history() # تحديث نافذة الحركات
            self.load_data()  # تحديث الجدول الرئيسي
            messagebox.showinfo("تم الاستقطاع", f"تم استقطاع {payment_amount:,.0f} د.ع بنجاح!")
            
        except ValueError:
            messagebox.showerror("خطأ", "الرجاء إدخال مبلغ صحيح.")

    tk.Button(pay_frame, text="تأكيد الاستقطاع ✔️", bg="#388e3c", fg="white", font=("Arial", 11, "bold"), command=make_payment).pack(side=tk.LEFT)
    
    refresh_history() # استدعاء أول مرة عند فتح النافذة

# ================= الميزات السابقة ================= #

def send_whatsapp(self):
    selected = self.tree.focus()
    if not selected:
        messagebox.showwarning("تنبيه", "يرجى تحديد زبون من القائمة لراسلته.")
        return
        
    values = self.tree.item(selected, 'values')
    date, remaining, paid, price, item, phone, name, id_val = values
    
    if not phone or phone.strip() == "":
        messagebox.showerror("خطأ", "لا يوجد رقم هاتف مسجل لهذا الزبون.")
        return
        
    phone = str(phone).strip()
    if phone.startswith("07"): phone = "+964" + phone[1:]
        
    message = f"مرحباً {name}،\nتفاصيل حسابك لدينا:\nالمادة: {item}\nالسعر الكلي: {price} د.ع\nالمدفوع: {paid} د.ع\nالمتبقي (الديون): {remaining} د.ع\nشكراً لتعاملك معنا."
    encoded_message = urllib.parse.quote(message)
    webbrowser.open(f"https://wa.me/{phone}?text={encoded_message}")

def print_info(self):
    selected = self.tree.focus()
    if not selected:
        messagebox.showwarning("تنبيه", "يرجى تحديد زبون لطباعة كشفه.")
        return
        
    values = self.tree.item(selected, 'values')
    date, remaining, paid, price, item, phone, name, id_val = values
    
    # جلب الحركات للطباعة
    self.c.execute("SELECT amount, payment_date FROM payments WHERE customer_id=? ORDER BY payment_id ASC", (id_val,))
    payments = self.c.fetchall()
    
    payments_html = "".join([f"<tr><td>{p[1]}</td><td>{p[0]:,.0f} د.ع</td></tr>" for p in payments])
    if not payments_html: payments_html = "<tr><td colspan='2' style='text-align:center;'>لا توجد حركات دفع مسجلة</td></tr>"

    html_content = f"""
    <html dir="rtl" lang="ar">
    <head>
        <meta charset="utf-8">
        <title>كشف حساب - {name}</title>
        <style>
            body {{ font-family: 'Segoe UI', Tahoma, Arial, sans-serif; padding: 40px; background: #fff; }}
            .container {{ border: 2px solid #000; padding: 20px; border-radius: 10px; max-width: 700px; margin: auto; }}
            h2 {{ color: #1e88e5; text-align: center; border-bottom: 2px solid #1e88e5; padding-bottom: 10px; }}
            table {{ width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 16px; }}
            th, td {{ border: 1px solid #000; padding: 10px; text-align: right; }}
            th {{ background-color: #f2f2f2; }}
            .print-btn {{ display: block; width: 200px; margin: 30px auto; padding: 10px; font-size: 18px; background: #4caf50; color: white; border: none; cursor: pointer; border-radius: 5px; text-align: center; }}
            @media print {{ .print-btn {{ display: none; }} }}
        </style>
    </head>
    <body>
        <div class="container">
            <h2>كشف حساب زبون</h2>
            <p><strong>اسم الزبون:</strong> {name}</p>
            <p><strong>رقم الهاتف:</strong> {phone}</p>
            <p><strong>تاريخ الإضافة:</strong> {date}</p>
            
            <table>
                <tr><th style="width:40%">المادة</th><td>{item}</td></tr>
                <tr><th>السعر الكلي</th><td>{price} د.ع</td></tr>
                <tr><th>الواصل (الكلي)</th><td>{paid} د.ع</td></tr>
                <tr><th style="color:red;">المتبقي (الديون)</th><td style="color:red; font-weight:bold;">{remaining} د.ع</td></tr>
            </table>
            
            <h3 style="margin-top:30px; text-align:center; color:#555;">سجل الدفعات والحركات</h3>
            <table>
                <tr><th>التاريخ والوقت</th><th>المبلغ المدفوع</th></tr>
                {payments_html}
            </table>
            
            <button class="print-btn" onclick="window.print()">🖨️ اضغط هنا للطباعة</button>
        </div>
    </body>
    </html>
    """
    
    file_name = f"invoice_{id_val}.html"
    with open(file_name, "w", encoding="utf-8") as f:
        f.write(html_content)
    webbrowser.open('file://' + os.path.realpath(file_name))

def delete_customer(self):
    selected = self.tree.focus()
    if not selected: return
    if messagebox.askyesno("تأكيد الحذف", "هل أنت متأكد من حذف الزبون وسجل دفعاته بالكامل؟"):
        customer_id = self.tree.item(selected, 'values')[7]
        self.c.execute("DELETE FROM payments WHERE customer_id=?", (customer_id,))
        self.c.execute("DELETE FROM customers WHERE id=?", (customer_id,))
        self.conn.commit()
        self.load_data()

if name == "main":
root = tk.Tk()
app = App(root)
root.mainloop()

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions