پرش به محتویات

ثبت دوطرفه

هر تراکنش دو چیز می‌نویسد: یک سطر Transaction که آدم می‌بیندش، و یک JournalEntry متوازن که دفترها را قابل اثبات می‌کند.

چرا زحمتش را بکشیم

دفتر یک‌طرفه یک فهرست است. می‌شود از آن پرسید «برای اجاره چقدر دادم؟» و جواب می‌دهد، اما نمی‌شود پرسید «این‌ها جور در می‌آید؟» — چیزی نیست که در برابرش بشود بررسی کرد.

ثبت دوطرفه همان سؤال را به تو می‌دهد. اگر بدهکار و بستانکار جور نباشند چیزی خراب است، و پیش از مشتری می‌فهمی.

شکل کار

Transaction                     JournalEntry
  flow: income                    ├── JournalLine  بدهکار  1000 (نقد)     ۲۵۰٬۰۰۰
  category: فروش                  └── JournalLine  بستانکار 4000 (درآمد)  ۲۵۰٬۰۰۰
  amount: 250,000

هزینه آینه‌اش است:

Transaction                     JournalEntry
  flow: expense                   ├── JournalLine  بدهکار  5000 (هزینه)  ۲٬۰۰۰٬۰۰۰
  category: اجاره                 └── JournalLine  بستانکار 1000 (نقد)   ۲٬۰۰۰٬۰۰۰

فهرست حساب‌ها

هنگام نیاز ساخته می‌شود، اولین باری که دفتری چیزی ثبت می‌کند:

کد نام نوع
1000 نقد و بانک دارایی
1900 خزانه دارایی
2000 بدهی بدهی
3000 سرمایه سرمایه
4000 درآمد درآمد
5000 هزینه هزینه

شش حساب کمینه‌ای است که یک دفتر برای ثبت هر چیزی لازم دارد. عمداً قابل‌پیکربندی نیست: مغازه‌دار نمی‌خواهد فهرست حساب طراحی کند، و سیستمی که اجازه‌اش را بدهد سیستمی است که اجازه می‌دهد فهرست خرابی طراحی کند.

آن ثابت، و جایی که بررسی می‌شود

debit, credit = await ledger.trial_balance(book_id)
assert debit == credit

آن ادعا تزئین نیست. پس از ثبت، پس از حذف تراکنش، پس از تسویهٔ بدهی، و پس از پرداخت قسط وام اجرا می‌شود — چون این‌ها همان چهار جایی هستند که نوشتن ناقص دفترها را اثبات‌ناپذیر می‌کند.

اگر سطرهایی که جور در نمی‌آیند به post_entry داده شوند BalanceError پرتاب می‌کند. در محیط عملیاتی هرگز شلیک نکرده، و نکته هم همین است: هست تا روزی که واقعاً چیزی خراب شود، سرِ نوشتن با صدای بلند شکست بخورد به‌جای اینکه سرِ گزارش بی‌صدا.

پول هرگز به اعشاری ممیز شناور دست نمی‌زند

shared/money.py نوع Money را تعریف می‌کند، یک TypeDecorator:

پستگرس NUMERIC(28, 4) — دهدهی دقیق بومی
SQLite String(40) — متن همان Decimal، که موقع خواندن برمی‌گردد

چهار رقم اعشار، چون نرخ تبدیل لازمشان دارد حتی وقتی ریال ندارد. بیست‌وهشت رقم، چون تورم هست.

روی مرز HTTP پول رشته است، نه عدد JSON. تنها نوع عددی JSON اعشاری ممیز شناور است؛ 12500000.15 می‌شود 12500000.149999999 و همان می‌ماند.

چندارزی

تراکنشی به ارز خارجی سه چیز ذخیره می‌کند:

original_amount    عددی که شخص تایپ کرد
original_currency  به چه ارزی تایپش کرد
conversion_rate    نرخ همان لحظه‌ای که تایپ کرد
converted_amount   اصلی × نرخ، به ارز پایهٔ دفتر

نرخ یک بار ثبت می‌شود و هرگز دوباره اعمال نمی‌شود. بازخوانی تراکنش سال گذشته نشان می‌دهد آن موقع چقدر می‌ارزید، نه آنچه نرخ امروز از آن می‌سازد. گزارش‌ها converted_amount را جمع می‌زنند، پس دفتری با پنج ارز هم جور در می‌آید.

حذف یک تراکنش

سند روزنامه را با خودش حذف می‌کند، در همان واحد کار. تراز آزمایشی بعدش در تست‌ها ادعا می‌شود، چون «سطر را حذف کن و سند را فراموش کن» دقیقاً همان ایرادی است که دفتری را اثبات‌ناپذیر می‌کند — و تا وقتی کسی مغایرت‌گیری نکند خودش را نشان نمی‌دهد.

جایی که سند نوشته نمی‌شود

تخصیص‌های خزانه و فیش‌های حقوقی سند تصمیم حقوقی‌اند، نه تراکنش. به‌جای ثبت در دفتر، روی دوره عکس‌برداری می‌شوند — فیش حقوقی بیانیهٔ چیزی است که به کسی بدهکاریم، و جابه‌جا شدن پول واقعیت جداگانه و بعدی است.

پولی که واقعاً جابه‌جا می‌شود — قسط وام پرداخت‌شده، شلیک قاعدهٔ تکرارشونده، و بدهی تسویه‌شده وقتی کاربر بخواهد پرداختش هم ثبت شود — مثل هر چیز دیگری از LedgerService.record() رد می‌شود و مثل هر چیز دیگری سند روزنامه‌اش را می‌گیرد. تسویهٔ بدهی به‌طور پیش‌فرض چیزی ثبت نمی‌کند: اگر فروش نسیه یا خرید قبلاً ثبت شده، پرداخت همان پول است و ثبت دوباره‌اش آن را دوبار می‌شمارد.