نظام مبيعات ومخزون في Excel بـ VBA — الكود مع الشرح (حصري)
هذه المقالة تشرح وحدة VBA متكاملة: دوال مساعدة، أحداث Worksheet_Change لشاشات الإدخال، إجراءات الترحيل (PostPurchase / PostSale / PostReturn / PostReturn_Linked)، أزرار تحميل الصور، حماية الأوراق، وتقارير الأرباح. بعد كل جزء سأضع فقرة توضيحية عن وظيفته وكيفية استخدامه أو تخصيصه.
1) رأس الموديول والإعدادات العامة + دوال المساعدة
الشرح (دوال المساعدة):
-
Option Explicit يجبرك تعلن المتغيرات صريحًا — خطوة مهمة لتقليل الأخطاء الإملائية في أسماء المتغيرات.
-
DATA_START_ROW ثابت يحدد من أين تبدأ بيانات الجداول (عادة صف 3 لأن الصفوف فوقه رؤوس). تغييره مركزي وسيسهل إعادة استخدام الكود لو كانت جداولك تبدأ في صف آخر.
-
MASTER_PWD ثابت مركزي لاستخدامه في إجراءات الحماية/فك الحماية — غيّره فورًا قبل نشر الملف. لا تترك كلمة السر نصاً واضحاً في ملف تشاركه علناً.
-
FindRowByCode(ws, code, codeCol) تقوم بالبحث عن قيمة code في عمود رقمه codeCol داخل الورقة ws وتُرجع رقم الصف إن وجد، أو 0 إن لم يجد. تستخدم Find مع LookAt:=xlWhole للبحث المطابق تمامًا. تُستخدم هذه الدالة كثيرًا لتحديد صف الصنف في ورقة stock.
-
NextDataRow(ws, keyCol) ترجع أول صف فارغ لإضافة سجل جديد بطريقة آمنة (لا تكتب فوق بيانات موجودة). تعتمد على عمود مرجعي keyCol للعثور على آخر صف مستخدم.
نصيحة عملية: احفظ هذا الجزء في موديول عام (Module) لأن الدوال تستخدم من أكثر من إجراء.
2) Worksheet_Change في شاشة المبيعات sales_rec (التعامل مع الكود/الاسم/الكمية/الخصم)
هذا الحدث يوضع داخل كود الورقة sales_rec (اضغط يمين على اسم الورقة → View Code → ألصق الكود داخل موديل الورقة).
الشرح (Worksheet_Change — sales_rec):
-
هذا الإيفينت يتتبع تغييرات المستخدم داخل الخلايا المحددة (
G6,G8,G16,G20
) فقط لتقليل العمل غير الضروري. -
عند تعديل كود الصنف (G6): يبحث في ورقة
stock
عن الكود في العمود A من الصف 3 نزولًا. إذا وجده، يملأ اسم الصنف (G8) وسعر البيع (G18) تلقائيًا. إن لم يجده يتم مسح الحقول ذات الصلة. -
عند تعديل اسم الصنف (G8): نفس المنطق لكن البحث في العمود B للعثور على الكود المقابل وملء الحقول. هذا يسهل الإدخال إما بالكود أو بالاسم.
-
عند تعديل الكمية (G16) أو الخصم (G20): يُعيد حساب الإجمالي (G22) بالصيغة
(كمية × سعر) − خصم
. يستخدمVal()
لتحويل النص إلى رقم بأمان. -
Application.EnableEvents = False/True
تُستخدم لمنع حدوث تكرار (recursion) — مهم جدًا لأن تعديل الخلايا داخل الحدث سيؤدي إلى استدعاء الحدث نفسه إن لم تقم بإيقاف الأحداث مؤقتًا.
نصيحة: لو عندك أعمدة إضافية (مثل ضريبة أو خصم إضافي) أضف خلاياها إلى Intersect
وحدث معادلة الإجمالي accordingly.
3) Worksheet_Change في شاشة المشتريات purch_rec
(حساب إجمالي الشراء)
هذا الحدث يوضع داخل كود ورقة purch_rec.
Dim qty As Double, price As DoubleOn Error GoTo SafeExit
‘ نفّذ الكود فقط لو الخلية المتغيرة I18 (كمية) أو I20 (سعر الشراء)
If Not Intersect(Target, Me.Range(“I18,I20”)) Is Nothing Then
Application.EnableEvents = False
qty = Val(Me.Range(“I18”).Value)
price = Val(Me.Range(“I20”).Value)
‘ لو فيه بيانات صحيحة احسب، غير كده امسح
If qty > 0 And price > 0 Then
Me.Range(“I22”).Value = qty * price
Else
Me.Range(“I22”).ClearContents
End If
End If
SafeExit:
Application.EnableEvents = True
End Sub
الشرح (Worksheet_Change — purch_rec):
-
يراقب تغييرات كمية الشراء (I18) وسعر الشراء (I20) ويحسب الإجمالي (I22).
-
بسيط وفعال لتجنب الأخطاء في حساب تكلفة الشراء قبل الترحيل.
4) زر الصورة Button21_Click — تحميل وعرض صورة الصنف من مسار مخزن في ورقة stock
الشرح (Button21_Click):
-
يبحث في ورقة stock عمود L عن مسار الصورة الموافقة لكود الصنف الموجود في sales_rec!G6.
-
إذا وُجد، يُدرج الصورة في الشيت sales_rec ويطابق أبعاد ومكان عنصر Image1 (شكل موجود مسبقًا). هذا مفيد لعرض صورة الصنف في شاشة الإدخال.
-
ملاحظة: Pictures.Insert يتوقع مسار ملف محلي أو مسار شبكي متاح. تأكد أن المسارات صحيحة وأن المستخدم لديه صلاحيات الوصول للملف.
-
تحذير عملي: كل Insert يضيف صورة جديدة كـ Picture مستقل — لو تريد استبدال الصورة السابقة ستحتاج لحذف الصور القديمة قبل الإدراج أو إعادة استخدامها بدل الإدراج المتكرر.
5) ماكروّات التنقل (Macro1..Macro9) وماكرو الترحيل المؤرشف sales_rec (نسخة أوتوماتيك سابقة)
الشرح (ماكروّات التنقل):
-
سلسة بسيطة لتسهيل التنقل بين الأوراق من أزرار على واجهة home أو أي لوحة. ضع كل زر في الواجهة ليستدعي الماكرو المناسب.
-
تعمل كاختصارات ولا تحتوي منطق بيانات.
ماكرو sales_rec القديم (نسخة نسخه المستخدم في ملفك — وظيفته نقل القيم ونسخها في sheet “sales”)
الشرح (ماكرو sales_rec القديم):
-
ماكرو يعتمد على تحديد النطاق ثم الانتقال لشيت sales ولصق القيم (Transpose) — طريقة قديمة لكنها تعمل. لكن بها نقاط ضعف: تعتمد على الخلايا النشطة والتنقل بالـ Select/Activate مما يجعلها هشة إذا تغيّر التصميم.
-
أنصح باستخدام إجراء PostSale (أسفله) لأنه أكثر أمانًا ومرونة ويكتب مباشرة للخلايا دون Select أو Paste.
6) Sub PostPurchase — ترحيل المشتريات وتحديث المخزون (كود منظم ومكتمل)
الشرح (PostPurchase):
-
يقرأ الحقول من شاشة purch_rec، يتحقق من صحة الكود والكمية، ثم يضيف صفًا في شيت purch.
-
في تحديث المخزون: إن وجد الصنف في stock (باستخدام FindRowByCode)، يعزز الكمية ويحدث سعر الشراء وتاريخ التوريد إن أمكن. إن لم يوجد الصنف يضيف صفًا جديدًا بالمعلومات الأساسية.
-
بعد الترحيل ينظف شاشة الإدخال. يُظهر رسالة تأكيد عند النجاح.
-
تصميم هذا الإجراء يجعل إدارة المشتريات آلية ويساعد في تتبع التوريدات وتاريخها.
7) Sub PostSale — النسخة المكتملة (ترحيل مبيعات، خصم من المخزون، تحذير الكمية)
هذا هو الإجراء الذي طلبت نسخه مكتملة ومتوافقة. ألصقه في Module العام (أو استعمله كـ Public Sub).
الشرح (PostSale — النسخة المكتملة):
-
يضمن هذا الإجراء ترحيل الفاتورة بأمان: فحص الحقول، إعادة حساب الإجمالي إذا لم يُحسب، إضافة السجل لورقة sales ثم تعديل الكمية في stock.
-
يتضمن تحذيرًا إن كانت الكمية في المخزون أقل من المباعة ويعطي المستخدم خيار الإلغاء. هذا يوازن بين مرونة العمل والحماية من الأخطاء.
-
إن لم يوجد الصنف في stock يضيف صفًا جديدًا مع كمية سالبة — مفيد لمعرفة الأصناف المباعة قبل إضافتها إلى المخزون.
-
بعد النجاح يُنظف شاشة الإدخال ويُرجع “عميل نقدي”.
نصيحة تنفيذية: لو تريد رفض تنفيذ البيع نهائيًا عند وجود كمية ناقصة (بدلاً من تحذير)، غير رسالة الـ MsgBox لإجبار المستخدم على تعديل الكمية أولاً.
8) Sub PostReturn و PostReturn_Linked — تسجيل المرتجعات وتعديل الفاتورة الأصلية
PostReturn (بسيط — يسجل المرتجع ويزيد المخزون)
‘ (كود مطابق لما أرفقته سابقاً — يسجل المرتجع في sheet “returns” ويزيد المخزون)
‘ يقوم بتنظيف شاشة الإدخال ويعيد “عميل نقدي”
End Sub
الشرح (PostReturn):
-
يتم ترحيل بيانات المرتجع إلى شيت returns ثم زيادة الكمية في stock. يناسب حالة تسجيل مرتجع مستقل دون تعديل الفاتورة الأصلية.
PostReturn_Linked (مترابط — يعدل الفاتورة الأصلية في sheet “sales” ثم يحدث المخزون)
الشرح (PostReturn_Linked):
-
هذه النسخة مميزة لأنها لا تكتفي بتسجيل المرتجع، بل تبحث عن الفاتورة الأصلية في شيت المبيعات (sales) وتُقلِّل الكمية والإجمالي لديها لتعكس المرتجع. هذا مهم للمحافظة على سجل مبيعات صحيح.
-
إن لم تجد الفاتورة الأصلية تُنبه المستخدم وتوقف العملية — هذا حماية قوية ضد أخطاء التسجيل.
-
بعد تعديل الفاتورة يُزاد المخزون بالكمية المرتجعة.
نصيحة: احتفظ بنسخة من شيت sales أو سجل للتغييرات قبل تشغيل PostReturn_Linked لأول مرة على بيانات حقيقية.
9) ResetSalesForm و ApplyProtection و SwitchUser و أدوات الحماية
On Error Resume Next
With ThisWorkbook.Worksheets(“sales_rec”)
.Range(“G6,G8,G10,G12,G16,G18,G20,G22,G24”).ClearContents
.Range(“G14”).Value = “عميل نقدي”
End With
With ThisWorkbook.Worksheets(“returns_rec”)
.Range(“G6,G8,G10,G12,G16,G18,G20,G22,G24”).ClearContents
.Range(“G14”).Value = “عميل نقدي”
End With
End SubSub ApplyProtection()
Dim ws As Worksheet
Dim PWD As String
PWD = MASTER_PWD ‘ استخدام الثابت الموحد
On Error Resume Next
For Each ws In ThisWorkbook.Worksheets
ws.Unprotect Password:=PWD
Next ws
On Error GoTo 0
‘ تفعيل القفل: نجعل الخلايا كلها مقفولة ثم نفتح خلايا شاشات الإدخال فقط
For Each ws In ThisWorkbook.Worksheets
ws.Cells.Locked = True
Next ws
With ThisWorkbook.Worksheets(“sales_rec”)
.Range(“G6,G8,G10,G12,G14,G16,G18,G20,G22,G24”).Locked = False
End With
With ThisWorkbook.Worksheets(“purch_rec”)
.Range(“I8,I10,I12,I14,I16,I18,I20,I22,I24”).Locked = False
End With
With ThisWorkbook.Worksheets(“returns_rec”)
.Range(“G6,G8,G10,G12,G14,G16,G18,G20,G22,G24”).Locked = False
End With
For Each ws In ThisWorkbook.Worksheets
ws.Protect Password:=PWD, UserInterfaceOnly:=True, AllowFiltering:=True
Next ws
MsgBox “تم تطبيق الحماية (عدل كلمة المرور في الكود إذا رغبت).”, vbInformation
End Sub
Sub SwitchUser()
Dim PWD As String
Dim ws As Worksheet
Dim managerMode As Boolean
managerMode = (ThisWorkbook.Worksheets(“home”).Visible = xlSheetVisible)
If managerMode = True Then
Call ShowEmployeeSheets
MsgBox “تم الرجوع لوضع الموظف.”, vbInformation
Else
PWD = InputBox(“ادخل كلمة المرور للدخول كمدير:”, “تبديل الدخول”)
If PWD = MASTER_PWD Then
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
For Each ws In ThisWorkbook.Worksheets
ws.Visible = xlSheetVisible
Next ws
ThisWorkbook.Worksheets(“home”).Activate
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
MsgBox “تم الدخول كمدير.”, vbInformation
Else
MsgBox “كلمة مرور غير صحيحة.”, vbCritical
End If
End If
End Sub
Sub UnprotectAllSheets()
Dim ws As Worksheet
For Each ws In ThisWorkbook.Worksheets
On Error Resume Next
ws.Unprotect Password:=MASTER_PWD
On Error GoTo 0
Next ws
End Sub
Sub ProtectAllSheets()
Dim ws As Worksheet
For Each ws In ThisWorkbook.Worksheets
On Error Resume Next
ws.Protect Password:=MASTER_PWD, DrawingObjects:=True, Contents:=True, Scenarios:=True
On Error GoTo 0
Next ws
End Sub
Sub ShowEmployeeSheets()
Dim ws As Worksheet
Dim adminSheets As Variant
adminSheets = Array(“home”, “report”, “profit_loss”)
For Each ws In ThisWorkbook.Worksheets
ws.Visible = xlSheetHidden
Next ws
Dim i As Long
For i = LBound(adminSheets) To UBound(adminSheets)
On Error Resume Next
ThisWorkbook.Worksheets(adminSheets(i)).Visible = xlSheetVisible
On Error GoTo 0
Next i
End Sub
الشرح (حماية وتبديل المستخدم):
-
ApplyProtection تفك الحماية ثم تعيد تطبيقها بحيث تكون خلايا إدخال النماذج مفتوحة فقط ويستطيع الماكروز الكتابة لأن UserInterfaceOnly:=True. لا تنسَ تغيير MASTER_PWD.
-
SwitchUser يوفّر طريقة بسيطة لتبديل بين وضع الموظف والوضع الإداري عبر كلمة مرور.
-
ShowEmployeeSheets تخفي أوراق المدير وتُظهر واجهة الموظف فقط. عدّل مصفوفة adminSheets حسب أوراقك الحساسة.
ملاحظة أمنية: لا تحفظ كلمة المرور في أماكن عامة. إذا كان الملف سيُشارك، فكّر في طرق أخرى للتحقق بدل حفظ كلمة سر في الكود (مثلاً تخزين هاش في مصدر خارجي).
10) تقرير الأرباح والخسائر UpdateProfitLoss — تجميع حسب شهور
Const DATA_START_ROW As Long = 3
Const PWD As String = “CHANGE_THIS_PASSWORD”Dim wsS As Worksheet, wsP As Worksheet, wsN As Worksheet, wsSt As Worksheet, wsPL As Worksheet
Dim salesArr, purchArr, expArr, staffArr
Dim m As Long, i As Long, lastRow As Long
Dim salesTot(1 To 12) As Double, purchTot(1 To 12) As Double
Dim expTot(1 To 12) As Double, staffTot(1 To 12) As Double
Dim d As Variant
Set wsS = ThisWorkbook.Worksheets(“sales”)
Set wsP = ThisWorkbook.Worksheets(“purch”)
Set wsN = ThisWorkbook.Worksheets(“nfaqat”)
Set wsSt = ThisWorkbook.Worksheets(“staff”)
Set wsPL = ThisWorkbook.Worksheets(“profit_loss”)
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
wsS.Unprotect Password:=PWD
wsP.Unprotect Password:=PWD
wsN.Unprotect Password:=PWD
wsSt.Unprotect Password:=PWD
wsPL.Unprotect Password:=PWD
wsPL.Range(“B3:M6”).ClearContents
‘ (يتبع: جمع المبيعات، المشتريات، النفقات، الرواتب حسب الشهر، ثم كتابة النتائج)
wsS.Protect Password:=PWD
wsP.Protect Password:=PWD
wsN.Protect Password:=PWD
wsSt.Protect Password:=PWD
wsPL.Protect Password:=PWD
wsPL.Activate
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
MsgBox “تم تحديث تقرير الأرباح والخسائر.”, vbInformation
End Sub
الشرح (UpdateProfitLoss):
-
يقوم بتجميع مبيعات ومشتريات ونفقات ومرتبات شهرية ثم يكتب النتائج في profit_loss.
-
لا أنصح تشغيله على جداول ضخمة بدون اختبار لأن نسخ البيانات إلى مصفوفات ثم التكرار قد يستهلك وقتًا مع بيانات كبيرة — لكن الهيكل جيد للتقارير الشهرية.
11) أين ألصق الكود خطوة بخطوة (تعليمات عملية)
-
افتح Excel → Alt + F11 لفتح محرر VBA.
-
لإضافة الدوال العامة والإجراءات (PostSale, PostPurchase, Helpers) → Insert → Module → ألصق الكود كله في موديل جديد.
-
لأحداث الورقة (Worksheet_Change) لـ sales_rec و purch_rec → في Project Explorer افتح الشيت المعني → دوبل كليك → ألصق كود الـ Worksheet_Change داخل هذا الـ Sheet module.
-
احفظ الملف كـ Excel Macro-Enabled Workbook (*.xlsm).
-
قبل الاختبار غيّر MASTER_PWD إلى كلمة سر قوية. اجري اختبار على نسخة من الملف.
12) نصائح فنية وأفضل ممارسات قبل التشغيل على بيانات حقيقية
-
نسخة احتياطية: لا تعمل على الملف الأصلي — جرب على نسخة.
-
تنظيف المدخلات: تأكد أن أعمدة stock مثل A (الكود), B (الاسم), H أو I (الأسعار والكميات) منسقة بشكل مناسب (نص/رقم).
-
التحقق من المسارات: لو تستخدم Button21_Click تأكد أن مسارات الصور صحيحة وملفات الصور متاحة.
-
سلوك الكميات السالبة: قرّر إن كنت تريد منع المبيعات التي تؤدي لمخزون سالب أم لا. الكود الآن يحذرك لكنه يسمح بالتسجيل.
-
سجلات التغيير (Audit log): لو مطلوب تتبع المستخدمين والتغييرات، أضف شيت audit_log وأدخل سجل مع التاريخ والمستخدم (Application.UserName) عند كل ترحيل.