في هذا المشروع سنبني برنامج إدارة مخزون بسيط باستخدام بايثون يعمل من Terminal. يمكنه إضافة الأصناف، عرض المخزون، البحث، زيادة الكمية، صرف كمية دون السماح بمخزون سالب، تعديل الاسم والسعر، حذف صنف بعد التأكيد، عرض الأصناف منخفضة المخزون، حساب قيمة المخزون، وحفظ البيانات تلقائيًا في ملف inventory.json.
عند تشغيل البرنامج ستظهر قائمة قريبة من:
1. عرض المخزون
2. إضافة صنف
3. البحث عن صنف
4. زيادة الكمية
5. صرف كمية
6. تعديل صنف
7. حذف صنف
8. الأصناف منخفضة المخزون
9. قيمة المخزون
0. خروج
هذا مشروع تعليمي للمبتدئ، وليس نظام ERP أو برنامجًا جاهزًا لإدارة متجر حقيقي.
{alertInfo} ما ستتدرب عليه: Dictionaries وFunctions وLoops وif وtry/except وJSON وpathlib وf-strings داخل برنامج واحد له بيانات تبقى محفوظة بعد إغلاقه.
{getToc} $title={محتوى المقال}
قبل أن نبدأ: ما مستوى المشروع؟
المشروع مناسب إذا كنت تعرف أساسيات المتغيرات والشروط والحلقات والدوال والقواميس وinput() والتعامل البسيط مع الأخطاء والملفات.
إذا احتجت مراجعة القواميس، راجع شرح Dictionaries في بايثون. ولتنظيم الكود إلى دوال راجع شرح Functions في بايثون.
1. تصميم بيانات المخزون
سنستخدم Product Code كمفتاح رئيسي، ثم نخزن داخل كل صنف الاسم والكمية والسعر:
inventory = {
"P001": {
"name": "Keyboard",
"quantity": 10,
"price": 25.0
},
"P002": {
"name": "Mouse",
"quantity": 15,
"price": 12.5
}
}
استخدام Code أفضل من الاعتماد على الاسم وحده؛ لأن الاسم قد يتكرر أو يتغير، بينما الكود يعطي كل صنف مفتاحًا واضحًا للبحث والتعديل.
في النسخة الأساسية نخزن أربع معلومات فقط: code وname وquantity وprice. لا نضيف المورد والباركود والضرائب والمخازن والفواتير حتى لا يتحول المشروع إلى نظام محاسبي.
2. لماذا نستخدم JSON؟
- لا تحتاج مكتبة خارجية.
- تخزن Dictionaries بصورة مناسبة.
- سهلة القراءة للمبتدئ.
- تحافظ على البيانات بعد إغلاق البرنامج.
لكن JSON ليست بديلًا مثاليًا عن قاعدة بيانات في نظام مخزون كبير. هذا مشروع تعليمي، والخطوة الطبيعية التالية بعد فهمه هي SQLite.
3. تحديد ملف البيانات
import json
from pathlib import Path
DATA_FILE = Path("inventory.json")
LOW_STOCK_LIMIT = 5
4. تحميل البيانات بأمان
def load_inventory():
if not DATA_FILE.exists():
return {}
try:
with DATA_FILE.open(
"r",
encoding="utf-8"
) as file:
inventory = json.load(file)
except json.JSONDecodeError as error:
raise SystemExit(
"ملف inventory.json غير صالح. "
"لن يتم تعديل الملف حتى تصلحه "
"أو تعيد تسميته."
) from error
if not isinstance(inventory, dict):
raise SystemExit(
"صيغة ملف المخزون غير متوقعة."
)
return inventory
إذا لم يوجد الملف يبدأ البرنامج بمخزون فارغ. وإذا كان JSON تالفًا، يتوقف البرنامج بدل إرجاع قاموس فارغ ثم الكتابة فوق الملف التالف دون انتباه.
5. حفظ المخزون
def save_inventory(inventory):
temp_file = DATA_FILE.with_name(
DATA_FILE.name + ".tmp"
)
with temp_file.open(
"w",
encoding="utf-8"
) as file:
json.dump(
inventory,
file,
ensure_ascii=False,
indent=4
)
temp_file.replace(DATA_FILE)
ensure_ascii=False يحفظ العربية بصورة مقروءة، وindent=4 يرتب JSON. ونكتب أولًا إلى ملف مؤقت ثم نستبدل الملف الأصلي.
للمراجعة العامة راجع التعامل مع الملفات في بايثون.
6. منع الإدخالات غير الصالحة
def read_non_negative_int(prompt):
while True:
try:
value = int(
input(prompt).strip()
)
except ValueError:
print("أدخل عددًا صحيحًا.")
continue
if value < 0:
print(
"القيمة لا يمكن أن تكون سالبة."
)
continue
return value
الكمية الأساسية تقبل الصفر لأن الصنف قد يكون Out of Stock. أما زيادة أو صرف المخزون فتستخدم دالة أخرى تفرض قيمة أكبر من صفر.
7. إضافة صنف
code = input(
"كود الصنف: "
).strip().upper()
if code in inventory:
print(
"هذا الكود مستخدم بالفعل."
)
نحوّل Code إلى أحرف كبيرة حتى تصبح p001 وP001 قيمة موحدة في هذا المشروع. الاسم الفارغ مرفوض، والكمية يجب أن تكون Integer غير سالبة، والسعر رقمًا غير سالب.
8. عرض المخزون
نستخدم f-strings لعرض Code والاسم والكمية والسعر والقيمة. لا نحتاج مكتبة خارجية للجداول.
{alertInfo} محاذاة النصوص العربية داخل Terminal قد تختلف بين البيئات بسبب RTL، لذلك أمثلة الناتج تستخدم أسماء منتجات إنجليزية حتى يكون الجدول أوضح.
9. البحث عن صنف
النسخة النهائية تبحث أولًا بـCode، ثم تستخدم جزءًا من الاسم إذا لم تجد الكود. هذا بحث بسيط مناسب للمبتدئ، وليس محرك بحث كاملًا.
10. زيادة الكمية
inventory[code]["quantity"] += quantity
save_inventory(inventory)
11. صرف كمية ومنع المخزون السالب
current_quantity = (
inventory[code]["quantity"]
)
if quantity > current_quantity:
print(
"الكمية المطلوبة أكبر "
"من المخزون المتاح."
)
return
inventory[code]["quantity"] -= (
quantity
)
save_inventory(inventory)
إذا كان المتاح 5 والمطلوب 8، لا تتغير البيانات.
12. الصفر مسموح
إذا صرفت آخر قطعة تصبح الكمية 0، ويبقى الصنف موجودًا. نفاد المخزون لا يعني أن المنتج يجب أن يُحذف.
13. تعديل الصنف
التعديل المباشر في المشروع مخصص للاسم والسعر. أما حركة الكمية فلها وظائف منفصلة حتى لا تختلط تعديلات بيانات المنتج بحركات Stock.
14. حذف صنف بعد التأكيد
confirmation = input(
f"هل تريد حذف {product_name}؟ "
"(y/n): "
).strip().casefold()
if confirmation not in {
"y",
"yes",
"نعم"
}:
print("تم إلغاء الحذف.")
return
15. الأصناف منخفضة المخزون
نستخدم:
LOW_STOCK_LIMIT = 5
وكل صنف كميته أقل من أو تساوي 5 يظهر في التقرير. الصنف ذو الكمية 0 يظهر باسم OUT OF STOCK.
16. حساب قيمة المخزون
total = sum(
product["quantity"]
* product["price"]
for product
in inventory.values()
)
هذا حساب تعليمي بسيط: الكمية × السعر. لا يعني Cost Valuation محاسبيًا، ولا يحتوي FIFO أو متوسطًا مرجحًا أو تكلفة شراء منفصلة.
17. القائمة الرئيسية
1. عرض المخزون
2. إضافة صنف
3. البحث عن صنف
4. زيادة الكمية
5. صرف كمية
6. تعديل صنف
7. حذف صنف
8. الأصناف منخفضة المخزون
9. قيمة المخزون
0. خروج
إذا أدخل المستخدم قيمة غير موجودة مثل 14 أو abc تظهر رسالة «اختيار غير صحيح» ويستمر البرنامج.
18. تنظيم البرنامج
الترتيب النهائي: Imports ثم Constants ثم دوال الإدخال، ثم التحميل والحفظ، ثم عمليات المخزون، ثم main()، وأخيرًا:
if __name__ == "__main__":
main()
هذا يجعل main() تعمل عند تشغيل الملف مباشرة.
19. الكود النهائي الكامل: inventory_manager.py
هذا هو الكود الكامل الجاهز للنسخ والتجربة:
import json
from math import isfinite
from pathlib import Path
DATA_FILE = Path("inventory.json")
LOW_STOCK_LIMIT = 5
def load_inventory():
if not DATA_FILE.exists():
return {}
try:
with DATA_FILE.open(
"r",
encoding="utf-8"
) as file:
inventory = json.load(file)
except json.JSONDecodeError as error:
raise SystemExit(
"ملف inventory.json غير صالح. "
"لن يتم تعديل الملف حتى تصلحه "
"أو تعيد تسميته."
) from error
except OSError as error:
raise SystemExit(
f"تعذر قراءة ملف المخزون: {error}"
) from error
if not isinstance(inventory, dict):
raise SystemExit(
"صيغة ملف المخزون غير متوقعة. "
"يجب أن يحتوي على JSON Object."
)
return inventory
def save_inventory(inventory):
temp_file = DATA_FILE.with_name(
DATA_FILE.name + ".tmp"
)
try:
with temp_file.open(
"w",
encoding="utf-8"
) as file:
json.dump(
inventory,
file,
ensure_ascii=False,
indent=4
)
temp_file.replace(DATA_FILE)
except OSError as error:
raise SystemExit(
f"تعذر حفظ المخزون: {error}"
) from error
def read_non_empty(prompt):
while True:
value = input(prompt).strip()
if value:
return value
print(
"القيمة لا يمكن أن تكون فارغة."
)
def read_non_negative_int(prompt):
while True:
try:
value = int(
input(prompt).strip()
)
except ValueError:
print(
"أدخل عددًا صحيحًا."
)
continue
if value < 0:
print(
"القيمة لا يمكن أن تكون سالبة."
)
continue
return value
def read_positive_int(prompt):
while True:
value = read_non_negative_int(
prompt
)
if value == 0:
print(
"أدخل كمية أكبر من صفر."
)
continue
return value
def read_non_negative_float(prompt):
while True:
try:
value = float(
input(prompt).strip()
)
except ValueError:
print(
"أدخل رقمًا صحيحًا أو عشريًا."
)
continue
if value < 0 or not isfinite(value):
print(
"السعر يجب أن يكون "
"رقمًا غير سالب."
)
continue
return value
def read_optional_non_negative_float(
prompt
):
while True:
raw_value = input(prompt).strip()
if not raw_value:
return None
try:
value = float(raw_value)
except ValueError:
print(
"أدخل رقمًا صحيحًا أو عشريًا، "
"أو اترك الحقل فارغًا."
)
continue
if value < 0 or not isfinite(value):
print(
"السعر يجب أن يكون "
"رقمًا غير سالب."
)
continue
return value
def normalize_code(value):
return value.strip().upper()
def print_product(code, product):
print(f"Code: {code}")
print(
f"Name: {product['name']}"
)
print(
f"Quantity: {product['quantity']}"
)
print(
f"Price: {product['price']:.2f}"
)
def display_inventory(inventory):
if not inventory:
print("المخزون فارغ.")
return
header = (
f"{'Code':<10}"
f"{'Name':<22}"
f"{'Qty':>8}"
f"{'Price':>12}"
f"{'Value':>14}"
)
print(header)
print("-" * len(header))
for code in sorted(inventory):
product = inventory[code]
quantity = product["quantity"]
price = product["price"]
value = quantity * price
print(
f"{code:<10}"
f"{product['name']:<22}"
f"{quantity:>8}"
f"{price:>12.2f}"
f"{value:>14.2f}"
)
def add_product(inventory):
code = normalize_code(
read_non_empty(
"كود الصنف: "
)
)
if code in inventory:
print(
"هذا الكود مستخدم بالفعل."
)
return
name = read_non_empty(
"اسم الصنف: "
)
quantity = read_non_negative_int(
"الكمية: "
)
price = read_non_negative_float(
"السعر: "
)
inventory[code] = {
"name": name,
"quantity": quantity,
"price": price
}
save_inventory(inventory)
print(
"تمت إضافة الصنف بنجاح."
)
def search_product(inventory):
if not inventory:
print("المخزون فارغ.")
return
term = input(
"أدخل كود الصنف أو جزءًا من الاسم: "
).strip()
if not term:
print(
"اكتب كودًا أو اسمًا للبحث."
)
return
code = normalize_code(term)
if code in inventory:
print_product(
code,
inventory[code]
)
return
matches = [
(product_code, product)
for product_code, product
in inventory.items()
if term.casefold()
in product["name"].casefold()
]
if not matches:
print(
"لم يتم العثور على صنف مطابق."
)
return
for product_code, product in matches:
print_product(
product_code,
product
)
print("-" * 30)
def restock_product(inventory):
code = normalize_code(
read_non_empty(
"كود الصنف: "
)
)
if code not in inventory:
print(
"الصنف غير موجود."
)
return
quantity = read_positive_int(
"الكمية المضافة: "
)
inventory[code]["quantity"] += (
quantity
)
save_inventory(inventory)
print(
"الكمية الجديدة:",
inventory[code]["quantity"]
)
def remove_stock(inventory):
code = normalize_code(
read_non_empty(
"كود الصنف: "
)
)
if code not in inventory:
print(
"الصنف غير موجود."
)
return
quantity = read_positive_int(
"الكمية المصروفة: "
)
current_quantity = (
inventory[code]["quantity"]
)
if quantity > current_quantity:
print(
"الكمية المطلوبة أكبر "
"من المخزون المتاح."
)
print(
"الكمية المتاحة:",
current_quantity
)
return
inventory[code]["quantity"] -= (
quantity
)
save_inventory(inventory)
print(
"الكمية الجديدة:",
inventory[code]["quantity"]
)
def update_product(inventory):
code = normalize_code(
read_non_empty(
"كود الصنف: "
)
)
if code not in inventory:
print(
"الصنف غير موجود."
)
return
product = inventory[code]
new_name = input(
"الاسم الجديد "
"(Enter للإبقاء على الحالي): "
).strip()
new_price = (
read_optional_non_negative_float(
"السعر الجديد "
"(Enter للإبقاء على الحالي): "
)
)
changed = False
if new_name:
product["name"] = new_name
changed = True
if new_price is not None:
product["price"] = new_price
changed = True
if not changed:
print(
"لم يتم تغيير أي بيانات."
)
return
save_inventory(inventory)
print(
"تم تحديث بيانات الصنف."
)
def delete_product(inventory):
code = normalize_code(
read_non_empty(
"كود الصنف: "
)
)
if code not in inventory:
print(
"الصنف غير موجود."
)
return
product_name = (
inventory[code]["name"]
)
confirmation = input(
f"هل تريد حذف {product_name}؟ "
"(y/n): "
).strip().casefold()
if confirmation not in {
"y",
"yes",
"نعم"
}:
print(
"تم إلغاء الحذف."
)
return
del inventory[code]
save_inventory(inventory)
print(
"تم حذف الصنف."
)
def show_low_stock(inventory):
low_stock_items = [
(code, product)
for code, product
in inventory.items()
if (
product["quantity"]
<= LOW_STOCK_LIMIT
)
]
if not low_stock_items:
print(
"لا توجد أصناف منخفضة المخزون."
)
return
print(
f"الحد المنخفض: {LOW_STOCK_LIMIT}"
)
for code, product in sorted(
low_stock_items
):
status = (
"OUT OF STOCK"
if product["quantity"] == 0
else "LOW STOCK"
)
print(
f"{code} | "
f"{product['name']} | "
f"Qty: {product['quantity']} | "
f"{status}"
)
def calculate_inventory_value(
inventory
):
total = sum(
product["quantity"]
* product["price"]
for product
in inventory.values()
)
print(
f"Total inventory value: "
f"{total:.2f}"
)
return total
def main():
inventory = load_inventory()
while True:
print(
"\n--- Inventory Manager ---"
)
print("1. عرض المخزون")
print("2. إضافة صنف")
print("3. البحث عن صنف")
print("4. زيادة الكمية")
print("5. صرف كمية")
print("6. تعديل صنف")
print("7. حذف صنف")
print(
"8. الأصناف منخفضة المخزون"
)
print("9. قيمة المخزون")
print("0. خروج")
choice = input(
"اختر: "
).strip()
if choice == "1":
display_inventory(
inventory
)
elif choice == "2":
add_product(
inventory
)
elif choice == "3":
search_product(
inventory
)
elif choice == "4":
restock_product(
inventory
)
elif choice == "5":
remove_stock(
inventory
)
elif choice == "6":
update_product(
inventory
)
elif choice == "7":
delete_product(
inventory
)
elif choice == "8":
show_low_stock(
inventory
)
elif choice == "9":
calculate_inventory_value(
inventory
)
elif choice == "0":
print(
"تم إنهاء البرنامج."
)
break
else:
print(
"اختيار غير صحيح."
)
if __name__ == "__main__":
main()
20. ملفات المشروع
inventory_project/
├── inventory_manager.py
└── inventory.json
inventory.json قد لا يظهر إلا بعد أول عملية تغيّر البيانات، مثل إضافة صنف.
21. تشغيل المشروع
python inventory_manager.pyوعلى بعض أجهزة Windows:
py inventory_manager.py
المشروع يعتمد فقط على مكتبات بايثون القياسية.
22. مثال تشغيل حقيقي
إضافة أول صنف:
اختر: 2
كود الصنف: P001
اسم الصنف: Keyboard
الكمية: 10
السعر: 25
تمت إضافة الصنف بنجاح.ثم عرض المخزون:
Code Name Qty Price Value
------------------------------------------------------------------
P001 Keyboard 10 25.00 250.00ثم صرف وحدتين:
اختر: 5
كود الصنف: P001
الكمية المصروفة: 2
الكمية الجديدة: 8ولو طلبت كمية أكبر من المتاح:
الكمية المطلوبة أكبر من المخزون المتاح.
الكمية المتاحة: 8
23. بيانات اختبار مناسبة
| Code | Name | Quantity | Price |
|---|---|---|---|
| P001 | Keyboard | 10 | 25 |
| P002 | Mouse | 15 | 12.5 |
| P003 | Monitor | 3 | 150 |
P003 سيظهر ضمن Low Stock لأن الكمية 3 والحد الافتراضي 5.
24. إعادة تشغيل البرنامج
أغلق البرنامج ثم شغله مرة ثانية. ستجد المنتجات موجودة لأن load_inventory() تقرأ inventory.json في بداية التشغيل.
25. شكل inventory.json
{
"P001": {
"name": "Keyboard",
"quantity": 8,
"price": 25.0
},
"P002": {
"name": "Mouse",
"quantity": 15,
"price": 12.5
}
}
إذا عدّلت JSON يدويًا وأفسدت بنيته، يتوقف البرنامج بدل الكتابة فوق الملف. راجع حل خطأ JSONDecodeError في بايثون عند الحاجة.
26. ماذا تعلمت من المشروع؟
| المفهوم | استخدامه |
|---|---|
| Dictionaries | تخزين المنتجات. |
| Functions | تقسيم البرنامج. |
| while | القائمة الرئيسية. |
| if / elif | الاختيارات والتحقق. |
| try / except | التعامل مع الإدخال وJSON. |
| JSON | حفظ البيانات. |
| pathlib | ملف البيانات. |
| f-strings | عرض الرسائل والجداول. |
يمكن مراجعة شرح try وexcept إذا كان التعامل مع الإدخالات غير الصحيحة جديدًا عليك.
27. أخطاء شائعة
- إدخال كمية نصية بدل رقم.
- إدخال كمية سالبة.
- إدخال سعر سالب أو غير رقمي.
- إضافة Code موجود مسبقًا.
- البحث عن Code غير موجود.
- صرف كمية أكبر من المتاح.
- إفساد
inventory.jsonيدويًا. - حذف المنتج الخطأ دون قراءة رسالة التأكيد.
- نسيان استدعاء الحفظ بعد عملية تغير البيانات.
28. هل يصلح لمتجر حقيقي؟
لا كحل جاهز. هو مشروع تعليمي. لا يحتوي قاعدة بيانات حقيقية أو Users أو صلاحيات أو فواتير أو سجل حركات أو نسخ احتياطي تجاري أو دعم أكثر من جهاز.
29. تطويرات مستقبلية
- SQLite بدل JSON.
- سجل لحركات المخزون.
- Purchase Price وSale Price.
- Supplier وBarcode.
- أكثر من Warehouse.
- تسجيل المبيعات.
- Login وصلاحيات.
- GUI أو Web App.
- Export CSV أو Excel.
- تقارير وتنبيه إعادة الطلب.
أفضل تطوير تالٍ: SQLite، لأنها تنقلك من ملف JSON واحد إلى قاعدة بيانات مناسبة أكثر للعمليات المتعددة.
30. مشاريع إضافية
بعد إكمال المشروع، راجع مشاريع بايثون للمبتدئين حسب المهارة أو حزمة 100 مشروع بايثون صغير.
الخلاصة
بنيت برنامج إدارة مخزون بسيطًا ببايثون يستخدم Dictionaries وFunctions وJSON، ويستطيع إضافة الأصناف والبحث والتعديل وزيادة وصرف الكميات ومنع المخزون السالب والحذف وعرض Low Stock وحساب قيمة المخزون.
{alertSuccess} الخطوة التالية: أضف ثلاثة منتجات، جرّب كل خيار في القائمة، ثم طوّر ميزة واحدة فقط بنفسك قبل الانتقال إلى SQLite.
أسئلة شائعة
كيف أبني برنامج إدارة مخزون ببايثون؟
ابدأ بقاموس يخزن المنتجات حسب Code، ثم قسّم العمليات إلى Functions واحفظ القاموس في JSON بعد كل تغيير.
كيف أحفظ بيانات المخزون بعد إغلاق البرنامج؟
استخدم json.dump() للحفظ وjson.load() للتحميل عند التشغيل التالي.
لماذا نستخدم JSON في هذا المشروع؟
لأنها بسيطة ومقروءة ولا تحتاج مكتبة خارجية، ومناسبة لمشروع تعليمي صغير.
كيف أمنع الكمية السالبة؟
قبل الصرف قارن الكمية المطلوبة بالمخزون الحالي؛ إذا كانت أكبر أوقف العملية.
كيف أبحث عن منتج في المخزون؟
استخدم Code كمفتاح مباشر في Dictionary، ويمكن إضافة بحث بسيط داخل الاسم.
كيف أحسب قيمة المخزون ببايثون؟
اضرب الكمية في السعر لكل صنف ثم اجمع القيم. هذا حساب تعليمي وليس Valuation محاسبيًا.
هل يصلح هذا البرنامج لمتجر حقيقي؟
لا كحل جاهز. هو مشروع تعليمي ولا يحتوي خصائص الأنظمة التجارية الفعلية.
متى أستخدم SQLite بدل JSON؟
عندما تكبر البيانات وتحتاج عمليات أكثر وتنظيمًا وسجلًا أفضل للتعديلات.
هل يمكن إضافة واجهة رسومية للمشروع؟
نعم، بعد التأكد أن منطق البرنامج يعمل جيدًا في Terminal يمكنك إضافة GUI لاحقًا.


