إذا أردت بناء مشروع بايثون عملي يجمع بين إدخال المستخدم، القوائم، الكلاسات، التحقق من البيانات، الحسابات المالية وإنشاء الملفات، فإن إنشاء فاتورة مبيعات باستخدام بايثون مثال ممتاز. لكن الفاتورة المفيدة ليست مجرد ضرب السعر في الكمية وطباعة الناتج؛ يجب أن تتعامل مع عدة منتجات، وتحسب الخصم والضريبة والإجمالي بدقة، وتمنع المدخلات غير الصحيحة، ثم تنتج ملفًا يمكن حفظه وطباعته.
في هذا المشروع سنبني برنامجًا يعمل من Terminal، يطلب بيانات الشركة والعميل والمنتجات، ويحسب Subtotal والخصم والضريبة والإجمالي النهائي، ثم ينشئ فاتورة PDF منظمة باستخدام مكتبة ReportLab.
وسنستخدم Decimal بدل float في الحسابات المالية. السبب أن الأسعار تحتاج إلى تمثيل عشري وتقريب يمكن التحكم فيه، بينما الأعداد العشرية الثنائية من نوع float قد تنتج فروقًا غير مرغوبة في التطبيقات المحاسبية.
{alertInfo} ماذا سيبني البرنامج؟ فاتورة متعددة المنتجات تحتوي على رقم فاتورة تلقائي، التاريخ، اسم الشركة، العميل، العملة، الكمية، سعر الوحدة، إجمالي كل سطر، المجموع الفرعي، الخصم، الضريبة، الإجمالي النهائي، ثم ملف PDF جاهز للحفظ أو الطباعة.
يمكنك تحميل ملفات المشروع الجاهزة، ثم فتح الكود وتجربته وتعديله على جهازك.
تحميل مشروع فاتورة المبيعات{getToc} $title={محتوى المقال}
لماذا اخترنا هذا الشكل للمشروع؟
نتائج البحث حول Invoice Generator في Python تتجه غالبًا إلى ثلاثة مستويات مختلفة:
- برنامج بسيط من Terminal يحسب الفاتورة.
- مشروع GUI باستخدام Tkinter أو أدوات مشابهة.
- مولد فاتورة PDF احترافية.
في هذا الدرس سنختار المستوى الذي يعطي أكبر فائدة بدون تحويل المقال إلى نظام مبيعات كامل: برنامج Terminal منظم + منطق مالي صحيح + تصدير PDF. لن نضيف قاعدة بيانات أو مخزونًا أو واجهة رسومية؛ هذه مكونات مناسبة لمشروع POS أكبر وليست ضرورية لفهم إنشاء الفاتورة نفسها.
مكونات فاتورة المبيعات التي سننشئها
| الجزء | وظيفته |
|---|---|
| Invoice Number | رقم فريد يسهل تمييز الفاتورة. |
| Issue Date | تاريخ إنشاء الفاتورة. |
| Company / Customer | بيانات البائع والعميل. |
| Items | عدة منتجات أو خدمات بدل عنصر واحد فقط. |
| Quantity | كمية كل عنصر. |
| Unit Price | سعر الوحدة. |
| Line Total | الكمية × سعر الوحدة. |
| Subtotal | مجموع عناصر الفاتورة قبل الخصم والضريبة. |
| Discount | خصم اختياري كنسبة مئوية. |
| Tax | ضريبة قابلة للتعديل حسب الحالة. |
| Total | المبلغ النهائي المستحق. |
{alertWarning} مهم: نسبة الضريبة في هذا المشروع ليست نسبة قانونية ثابتة. المستخدم يدخلها بنفسه. قوانين الضرائب والفوترة الإلكترونية تختلف حسب الدولة، لذلك هذا المشروع لا يُعد نظام فوترة إلكترونية قانونيًا جاهزًا ولا يطبق معيارًا ضريبيًا لدولة معينة.
تثبيت ReportLab لإنشاء PDF
سنستخدم مكتبة ReportLab لأنها مخصصة لإنشاء ملفات PDF برمجيًا وتوفر جداول وتنسيقات مناسبة لبناء فاتورة.
python -m pip install reportlab
بعد التثبيت يمكنك اختبار أن المكتبة متاحة:
import reportlab
print(reportlab.Version)
لماذا نستخدم Decimal بدل float للأسعار؟
في التطبيقات التعليمية الصغيرة قد ترى الأسعار مكتوبة باستخدام float. لكن عند بناء فاتورة، الأفضل استخدام Decimal لأنك تريد حسابًا عشريًا يمكن التحكم في تقريبه إلى منزلتين.
from decimal import Decimal
price = Decimal("19.99")
quantity = Decimal("3")
total = price * quantity
print(total)
وفي المشروع سننشئ دالة round_money() تستخدم ROUND_HALF_UP لتقريب القيم المالية إلى منزلتين:
def round_money(value: Decimal) -> Decimal:
return value.quantize(
Decimal("0.01"),
rounding=ROUND_HALF_UP
)
تصميم بيانات كل منتج في الفاتورة
بدل تخزين أسماء المنتجات والأسعار في قوائم منفصلة، سنمثل كل سطر داخل الفاتورة بكائن InvoiceItem. هذا يجعل الكود أوضح ويمنع خلط كمية منتج بسعر منتج آخر.
@dataclass(frozen=True)
class InvoiceItem:
description: str
quantity: Decimal
unit_price: Decimal
@property
def line_total(self) -> Decimal:
return round_money(self.quantity * self.unit_price)
كما أن الكلاس الكامل يتحقق من أن اسم المنتج غير فارغ، وأن الكمية أكبر من صفر، وأن سعر الوحدة غير سالب.
كيف نحسب الخصم والضريبة والإجمالي؟
ترتيب الحساب في هذا المشروع هو: إجمالي كل سطر، ثم المجموع الفرعي، ثم الخصم، ثم الضريبة على المبلغ بعد الخصم، وأخيرًا الإجمالي النهائي.
subtotal = invoice.subtotal
discount_amount = invoice.discount_amount
taxable_amount = invoice.taxable_amount
tax_amount = invoice.tax_amount
total = invoice.total
مثال: إذا كان المجموع الفرعي 66.00 والخصم 10%، يصبح الخصم 6.60 والمبلغ بعد الخصم 59.40. إذا كانت الضريبة 5% تصبح 2.97، والإجمالي النهائي 62.37.
{alertWarning} ترتيب الخصم والضريبة يختلف حسب القاعدة المحاسبية أو النظام الضريبي الذي تعمل به. الصيغة هنا مثال برمجي واضح، وليست قاعدة قانونية عامة لكل الدول.
الكود الكامل لإنشاء فاتورة مبيعات وحفظها PDF
احفظ الكود التالي في ملف باسم sales_invoice.py:
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import date
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from pathlib import Path
from uuid import uuid4
from xml.sax.saxutils import escape
from reportlab.lib import colors
from reportlab.lib.enums import TA_RIGHT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
MONEY_STEP = Decimal("0.01")
HUNDRED = Decimal("100")
def round_money(value: Decimal) -> Decimal:
"""Round a monetary value to two decimal places using half-up rounding."""
return value.quantize(MONEY_STEP, rounding=ROUND_HALF_UP)
def make_invoice_number() -> str:
"""Create a readable invoice number with a short random suffix."""
suffix = uuid4().hex[:6].upper()
return f"INV-{date.today():%Y%m%d}-{suffix}"
def format_quantity(value: Decimal) -> str:
"""Display quantities without unnecessary trailing decimal zeros."""
return format(value.normalize(), "f")
def format_money(value: Decimal, currency: str) -> str:
"""Format a monetary value for display in the invoice PDF."""
return f"{round_money(value):,.2f} {currency}"
@dataclass(frozen=True)
class InvoiceItem:
description: str
quantity: Decimal
unit_price: Decimal
def __post_init__(self) -> None:
description = self.description.strip()
if not description:
raise ValueError("Item description cannot be empty.")
if self.quantity <= 0:
raise ValueError("Quantity must be greater than zero.")
if self.unit_price < 0:
raise ValueError("Unit price cannot be negative.")
object.__setattr__(self, "description", description)
@property
def line_total(self) -> Decimal:
return round_money(self.quantity * self.unit_price)
@dataclass
class SalesInvoice:
company_name: str
customer_name: str
currency: str = "USD"
tax_rate: Decimal = Decimal("0")
discount_rate: Decimal = Decimal("0")
invoice_number: str = field(default_factory=make_invoice_number)
issue_date: date = field(default_factory=date.today)
items: list[InvoiceItem] = field(default_factory=list)
def __post_init__(self) -> None:
self.company_name = self.company_name.strip()
self.customer_name = self.customer_name.strip()
self.currency = self.currency.strip().upper()
if not self.company_name:
raise ValueError("Company name cannot be empty.")
if not self.customer_name:
raise ValueError("Customer name cannot be empty.")
if not self.currency:
raise ValueError("Currency cannot be empty.")
self._validate_percentage(self.tax_rate, "Tax rate")
self._validate_percentage(self.discount_rate, "Discount rate")
@staticmethod
def _validate_percentage(value: Decimal, label: str) -> None:
if value < 0 or value > 100:
raise ValueError(f"{label} must be between 0 and 100.")
def add_item(self, description: str, quantity: Decimal, unit_price: Decimal) -> None:
self.items.append(InvoiceItem(description, quantity, unit_price))
@property
def subtotal(self) -> Decimal:
return round_money(sum((item.line_total for item in self.items), Decimal("0")))
@property
def discount_amount(self) -> Decimal:
return round_money(self.subtotal * self.discount_rate / HUNDRED)
@property
def taxable_amount(self) -> Decimal:
return round_money(self.subtotal - self.discount_amount)
@property
def tax_amount(self) -> Decimal:
return round_money(self.taxable_amount * self.tax_rate / HUNDRED)
@property
def total(self) -> Decimal:
return round_money(self.taxable_amount + self.tax_amount)
def generate_invoice_pdf(invoice: SalesInvoice, output_dir: str | Path = "invoices") -> Path:
"""Generate a styled PDF invoice and return its path."""
if not invoice.items:
raise ValueError("Cannot generate an invoice without at least one item.")
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
pdf_path = output_path / f"{invoice.invoice_number}.pdf"
doc = SimpleDocTemplate(
str(pdf_path),
pagesize=A4,
rightMargin=15 * mm,
leftMargin=15 * mm,
topMargin=15 * mm,
bottomMargin=15 * mm,
title=f"Sales Invoice {invoice.invoice_number}",
author=invoice.company_name,
)
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
"InvoiceTitle",
parent=styles["Heading1"],
fontName="Helvetica-Bold",
fontSize=22,
leading=26,
alignment=TA_RIGHT,
textColor=colors.HexColor("#1F2937"),
spaceAfter=4 * mm,
)
label_style = ParagraphStyle(
"InvoiceLabel",
parent=styles["Normal"],
fontName="Helvetica-Bold",
fontSize=9,
textColor=colors.HexColor("#4B5563"),
)
normal_style = ParagraphStyle(
"InvoiceNormal",
parent=styles["Normal"],
fontName="Helvetica",
fontSize=10,
leading=14,
textColor=colors.HexColor("#111827"),
)
story = []
header = Table(
[
[
Paragraph(f"<b>{escape(invoice.company_name)}</b>", styles["Heading2"]),
Paragraph("SALES INVOICE", title_style),
]
],
colWidths=[90 * mm, 90 * mm],
)
header.setStyle(TableStyle([("VALIGN", (0, 0), (-1, -1), "TOP")]))
story.append(header)
story.append(Spacer(1, 4 * mm))
details = Table(
[
[Paragraph("BILL TO", label_style), Paragraph("INVOICE DETAILS", label_style)],
[
Paragraph(escape(invoice.customer_name), normal_style),
Paragraph(
f"Invoice #: {escape(invoice.invoice_number)}<br/>"
f"Date: {invoice.issue_date.isoformat()}<br/>"
f"Currency: {escape(invoice.currency)}",
normal_style,
),
],
],
colWidths=[90 * mm, 90 * mm],
)
details.setStyle(
TableStyle(
[
("VALIGN", (0, 0), (-1, -1), "TOP"),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
]
)
)
story.append(details)
story.append(Spacer(1, 7 * mm))
item_rows = [["Description", "Qty", "Unit Price", "Line Total"]]
for item in invoice.items:
item_rows.append(
[
Paragraph(escape(item.description), normal_style),
format_quantity(item.quantity),
format_money(item.unit_price, invoice.currency),
format_money(item.line_total, invoice.currency),
]
)
items_table = Table(
item_rows,
colWidths=[80 * mm, 20 * mm, 40 * mm, 40 * mm],
repeatRows=1,
)
items_table.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#111827")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("ALIGN", (1, 0), (-1, -1), "RIGHT"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#D1D5DB")),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F9FAFB")]),
("TOPPADDING", (0, 0), (-1, -1), 7),
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
]
)
)
story.append(items_table)
story.append(Spacer(1, 7 * mm))
summary_rows = [
["Subtotal", format_money(invoice.subtotal, invoice.currency)],
[f"Discount ({invoice.discount_rate}%)", format_money(invoice.discount_amount, invoice.currency)],
[f"Tax ({invoice.tax_rate}%)", format_money(invoice.tax_amount, invoice.currency)],
["TOTAL", format_money(invoice.total, invoice.currency)],
]
summary = Table(summary_rows, colWidths=[45 * mm, 45 * mm], hAlign="RIGHT")
summary.setStyle(
TableStyle(
[
("ALIGN", (1, 0), (1, -1), "RIGHT"),
("FONTNAME", (0, -1), (-1, -1), "Helvetica-Bold"),
("FONTSIZE", (0, -1), (-1, -1), 11),
("LINEABOVE", (0, -1), (-1, -1), 1, colors.HexColor("#111827")),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
]
)
)
story.append(summary)
story.append(Spacer(1, 12 * mm))
story.append(Paragraph("Thank you for your business.", normal_style))
doc.build(story)
return pdf_path
def read_non_empty(prompt: str) -> str:
while True:
value = input(prompt).strip()
if value:
return value
print("Value cannot be empty.")
def read_decimal(prompt: str, *, minimum: Decimal | None = None) -> Decimal:
while True:
raw = input(prompt).strip()
try:
value = Decimal(raw)
except InvalidOperation:
print("Enter a valid number, for example 2 or 19.95.")
continue
if minimum is not None and value < minimum:
print(f"Value must be at least {minimum}.")
continue
return value
def read_percentage(prompt: str) -> Decimal:
while True:
value = read_decimal(prompt, minimum=Decimal("0"))
if value <= 100:
return value
print("Percentage must be between 0 and 100.")
def build_invoice_from_input() -> SalesInvoice:
print("\n=== Sales Invoice Generator ===")
company_name = read_non_empty("Company name: ")
customer_name = read_non_empty("Customer name: ")
currency = read_non_empty("Currency code (e.g. USD, SAR, YER): ").upper()
discount_rate = read_percentage("Discount % (0 for none): ")
tax_rate = read_percentage("Tax % (0 for none): ")
invoice = SalesInvoice(
company_name=company_name,
customer_name=customer_name,
currency=currency,
tax_rate=tax_rate,
discount_rate=discount_rate,
)
print("\nAdd invoice items. Leave the item name empty when finished.")
while True:
description = input("Item name: ").strip()
if not description:
if invoice.items:
break
print("Add at least one item before finishing.")
continue
quantity = read_decimal("Quantity: ", minimum=Decimal("0.000001"))
unit_price = read_decimal("Unit price: ", minimum=Decimal("0"))
invoice.add_item(description, quantity, unit_price)
print(f"Added: {description}\n")
return invoice
def main() -> None:
invoice = build_invoice_from_input()
pdf_path = generate_invoice_pdf(invoice)
print("\n=== Invoice Summary ===")
print(f"Invoice number: {invoice.invoice_number}")
print(f"Subtotal: {format_money(invoice.subtotal, invoice.currency)}")
print(f"Discount: {format_money(invoice.discount_amount, invoice.currency)}")
print(f"Tax: {format_money(invoice.tax_amount, invoice.currency)}")
print(f"Total: {format_money(invoice.total, invoice.currency)}")
print(f"PDF saved to: {pdf_path.resolve()}")
if __name__ == "__main__":
main()
كيف تشغل برنامج الفاتورة؟
ضع نفسك في المجلد الذي يحتوي على sales_invoice.py ثم شغل:
python sales_invoice.py
سيطلب البرنامج اسم الشركة والعميل ورمز العملة ونسبتي الخصم والضريبة، ثم يسمح لك بإضافة عدة منتجات. عندما تنتهي، اترك اسم المنتج فارغًا واضغط Enter. إذا لم تضف أي عنصر، لن يسمح لك البرنامج بإنهاء الفاتورة.
مثال بيانات للتجربة
Company name: Arab Python Store
Customer name: Ahmed Ali
Currency code (e.g. USD, SAR, YER): USD
Discount % (0 for none): 10
Tax % (0 for none): 5
Item name: Keyboard
Quantity: 2
Unit price: 25.50
Item name: Mouse
Quantity: 1
Unit price: 15
Item name:
| Subtotal | 66.00 USD |
| Discount 10% | 6.60 USD |
| Taxable Amount | 59.40 USD |
| Tax 5% | 2.97 USD |
| Total | 62.37 USD |
أين يتم حفظ الفاتورة؟
ينشئ البرنامج مجلدًا باسم invoices تلقائيًا، ثم يحفظ داخله ملف PDF باسم رقم الفاتورة، مثل:
invoices/INV-20261004-A1B2C3.pdf
رقم الفاتورة يتكون من التاريخ مع جزء عشوائي قصير باستخدام uuid4() لتقليل احتمال التكرار.
كيف يبدو ملف PDF الناتج؟
الفاتورة الناتجة تحتوي على رأس واضح، بيانات العميل، رقم الفاتورة والتاريخ، ثم جدول للمنتجات بأعمدة Description وQty وUnit Price وLine Total. وفي الأسفل تظهر قيم Subtotal وDiscount وTax وTOTAL.
يستخدم الجدول repeatRows=1، لذلك إذا كثرت المنتجات وانتقلت الفاتورة إلى أكثر من صفحة يتكرر رأس الجدول تلقائيًا.
التحقق من المدخلات في برنامج الفاتورة
الدالة read_decimal() تعيد الطلب عندما يكتب المستخدم قيمة غير رقمية، كما ترفض الكمية الصفرية أو السالبة والأسعار السالبة. كذلك read_percentage() تقبل فقط نسبة من 0 إلى 100.
إذا كنت تريد فهم هذه الفكرة بتفصيل أكبر، راجع شرح try وexcept في Python والتعامل مع الأخطاء.
لماذا الكود مقسم إلى Classes وFunctions؟
| الجزء | المسؤولية |
|---|---|
InvoiceItem | يمثل منتجًا أو خدمة واحدة داخل الفاتورة. |
SalesInvoice | يحفظ بيانات الفاتورة ويحسب المجاميع. |
generate_invoice_pdf() | يحول بيانات الفاتورة إلى ملف PDF. |
read_decimal() | يتحقق من الإدخال الرقمي. |
build_invoice_from_input() | يجمع بيانات المستخدم من Terminal. |
main() | يربط خطوات البرنامج ويشغله. |
هذا الفصل يجعل المشروع أسهل في الاختبار، ويسمح لاحقًا باستبدال Terminal بواجهة GUI أو Web بدون إعادة كتابة منطق الحساب.
لماذا استخدمنا escape() مع النصوص؟
ReportLab تسمح باستخدام تنسيق شبيه بالـMarkup داخل Paragraph. لذلك تمر بيانات المستخدم مثل اسم الشركة والعميل ووصف المنتج عبر escape() قبل إدراجها، حتى لا تُفسر رموز مثل < أو & على أنها تنسيق.
هل يدعم هذا الكود العربية داخل PDF؟
البرنامج نفسه يتعامل مع Unicode، لكن تصميم PDF الأساسي هنا يستخدم خطوط PDF القياسية مثل Helvetica حتى يعمل بدون ملفات خطوط إضافية. هذه الخطوط ليست مناسبة لإخراج العربية بصورة احترافية.
إذا أردت فاتورة عربية بالكامل، تحتاج إلى خط TTF يدعم العربية مع RTL وText Shaping. الإصدارات الحديثة من ReportLab بدأت دعم العربية والـRTL وHarfBuzz، لكن التوثيق الرسمي ما زال يصف الدعم بأنه تجريبي ويحتاج إعدادًا إضافيًا.
{alertInfo} استخدم النسخة الحالية لفهم منطق الفاتورة وتوليد PDF بشكل موثوق. تعريب PDF بالكامل يستحق درسًا مستقلًا لأن الخطوط وتشكيل الحروف واتجاه RTL موضوع مختلف عن منطق الفاتورة.
هل نحتاج Tkinter أو قاعدة بيانات؟
ليس لإنشاء فاتورة واحدة. كثير من نتائج البحث تجمع Tkinter وSQLite والمخزون والفواتير، لكن ذلك يحول الموضوع إلى نظام مبيعات كامل. بعد إتقان هذا المشروع يمكنك إضافة GUI أو SQLite أو قائمة منتجات وحالة دفع وتاريخ استحقاق.
أخطاء شائعة عند برمجة فاتورة مبيعات
1. استخدام float لكل الحسابات المالية
Decimal أنسب عندما تريد دقة عشرية وتقريبًا واضحًا للأسعار.
2. السماح بكمية سالبة أو صفرية
تحقق من القيم قبل إضافتها إلى الفاتورة.
3. وضع نسبة ضريبة ثابتة وكأنها صحيحة في كل بلد
اجعل النسبة إعدادًا أو إدخالًا وطبّق القوانين الفعلية في مشروعك.
4. حساب الضريبة والخصم بدون تحديد الترتيب
يجب أن يكون ترتيب الحساب واضحًا لأن النتيجة قد تختلف حسب القاعدة المحاسبية.
5. إنشاء PDF بدون فصل منطق الحساب عن التصميم
اجعل SalesInvoice مسؤولة عن الحسابات ودالة PDF مسؤولة عن العرض فقط.
6. اعتبار المشروع نظام فوترة قانونيًا بمجرد أنه ينتج PDF
ملف PDF منظم لا يعني أن النظام يحقق متطلبات الفاتورة الضريبية أو الإلكترونية في بلدك.
اختبارات أجريت على الكود قبل نشره
- فحص Syntax للملف كاملًا.
- اختبار فاتورة بأكثر من عنصر.
- اختبار خصم
10%وضريبة5%والناتج النهائي62.37. - اختبار التقريب من
19.995إلى20.00. - رفض اسم منتج فارغ.
- رفض كمية تساوي صفرًا.
- رفض سعر سالب.
- رفض نسبة ضريبة خارج
0–100. - منع إنشاء PDF بدون منتجات.
- إنشاء فاتورة اختبار تحتوي على 80 عنصرًا والتأكد من خروجها في أربع صفحات مع تكرار رأس الجدول وظهور المجاميع في النهاية دون قص.
روابط مفيدة من بايثون العرب
- حساب الخصم والسعر النهائي في Python
- شرح try وexcept في Python والتعامل مع الأخطاء
- شرح Lists في Python للمبتدئين
مصادر رسمية للتوسع
- توثيق Decimal الرسمي في Python
- ReportLab PDF Generation User Guide
- توثيق دعم العربية وRTL في ReportLab
الخلاصة
أنشأنا فاتورة مبيعات باستخدام بايثون تدعم عدة منتجات، وتستخدم Decimal للحسابات المالية، وتتحقق من المدخلات، وتنشئ رقم فاتورة تلقائيًا، وتحسب الخصم والضريبة والإجمالي النهائي، ثم تحفظ النتيجة في PDF منظم.
الميزة الأهم ليست شكل PDF فقط، بل فصل منطق الحساب عن إدخال المستخدم وعن إنشاء الملف؛ وهذا يجعل المشروع قابلًا للتطوير لاحقًا إلى GUI أو قاعدة بيانات أو نظام مبيعات أكبر.
{alertSuccess} الخطوة التالية المنطقية: بعد التأكد من صحة الحسابات، أضف SQLite لحفظ الفواتير ثم شاشة للبحث عن فاتورة سابقة.
أسئلة شائعة
كيف أنشئ فاتورة مبيعات باستخدام بايثون؟
اجمع بيانات العميل والمنتجات، واحسب إجمالي كل سطر والمجموع الفرعي والخصم والضريبة، ثم احفظ النتيجة. في هذا المشروع نستخدم ReportLab لإنشاء PDF.
ما أفضل مكتبة لإنشاء فاتورة PDF في Python؟
توجد خيارات مثل ReportLab وfpdf2 وأدوات HTML-to-PDF. اخترنا ReportLab هنا لأنها مناسبة لإنشاء جداول وPDF مباشرة من Python.
لماذا استخدمنا Decimal بدل float؟
لأن Decimal توفر تمثيلًا عشريًا وتحكمًا واضحًا في التقريب، وهو مناسب أكثر للحسابات المالية.
هل يمكن إضافة أكثر من منتج؟
نعم. البرنامج يخزن العناصر في قائمة من InvoiceItem ويمكنك إضافة أي عدد قبل إنهاء الإدخال.
هل يدعم البرنامج الخصم والضريبة؟
نعم، ويقبل كلًا منهما كنسبة من 0 إلى 100. في المثال يتم تطبيق الخصم أولًا ثم حساب الضريبة على المبلغ بعد الخصم.
هل نسبة الضريبة ثابتة؟
لا. المستخدم يدخل النسبة، ويجب ضبط طريقة الحساب وفق النظام المحاسبي أو القانوني الفعلي الذي تعمل به.
هل هذا نظام فواتير كامل؟
لا. هو Invoice Generator عملي يركز على منطق الفاتورة وتصدير PDF. النظام الكامل قد يحتاج قاعدة بيانات ومخزونًا وحالات دفع ومتطلبات قانونية أخرى.
هل يمكن تحويله إلى برنامج بواجهة رسومية؟
نعم. لأن منطق الفاتورة منفصل عن الإدخال، يمكنك لاحقًا استخدام Tkinter أو Flet أو واجهة ويب واستدعاء الكلاسات نفسها.


