49 KiB
سند اجرایی فازبهفاز چندارزی عملیاتی — حسابیکس
نوع سند: برنامه اجرایی (Execution Plan)
وضعیت: پیشنویس اجرایی — بدون اعمال کد در این مرحله
مخاطب: مالک محصول، معمار، Backend، Flutter، QA
مرجع وضعیت فعلی: گزارش تحلیل چندارزی + MULTI_CURRENCY_IMPLEMENTATION_GUIDE.md
اصل غیرقابلمذاکره: کسبوکار تکارزی نباید هیچ UI/جریان کاری چندارزی غیرضروری ببیند.
۰. هدف و تعریف موفقیت
۰.۱ هدف نهایی
رساندن حسابیکس از «چندارزی سطح سند + نرخ + گزارش محدود» به «چندارزی عملیاتی قابلاعتماد» با این قابلیتها:
- فاکتور/خرید/دریافت/پرداخت/انتقال با ارز مناسب
- تسویه بینارزی با نرخ تراکنش
- مانده طرفحساب به تفکیک ارز (+ معادل پایه)
- گزارشهای یکدست (فیلتر ارز / همه ارزها با تسعیر شفاف)
- تسعیر پایان دوره (در صورت تصمیم محصول)
- رفتار صفرِ اصطکاک برای کسبوکار تکارزی
۰.۲ تعریف رسمی حالتها
MC = OFF ⇔ کسبوکار فقط default_currency دارد (بدون ارز فرعی در business_currencies)
MC = ON ⇔ حداقل یک ارز فرعی ≠ default_currency_id در business_currencies
منبع حقیقت کلاینت: AuthStore.isMultiCurrency از فیلد API is_multi_currency.
۰.۳ معیار موفقیت کلی (Definition of Done محصول)
| معیار | تکارزی (MC=OFF) | چندارزی (MC=ON) |
|---|---|---|
| UI | عین امروز؛ بدون فیلد/منوی FX | قابلیتهای فازهای منتشرشده دیده میشود |
| داده قدیمی | بدون تغییر معنایی | خواندنی و گزارشپذیر با قوانین مهاجرت |
| پرداخت همارز | بدون تغییر | بدون تغییر |
| پرداخت بینارزی | غیرقابلدسترسی | طبق سیاست فاز ۳ |
| گزارش | بدون dropdown ارز | فیلتر + حالت «همه» شفاف |
۱. وضعیت پایه (Baseline) — چه چیزی از قبل هست
| قطعه | وضعیت | یادداشت |
|---|---|---|
| کاتالوگ ارز + business_currencies | ✅ | |
| ارز پایه کسبوکار | ✅ | پس از تنظیم اولیه قابل تغییر نیست |
is_multi_currency + MultiCurrencyGate |
✅ | فاز ۰ |
تاریخچه نرخ business_currency_rates |
✅ | یک نرخ واحد نسبت به پایه |
snapshot فاکتور extra_info.fx |
✅ | |
| نرخ روز toolbar + apply-from-global | ✅ | فاز ۱ |
| auto-sync نرخ از اسنپشات مرکزی | ✅ | فاز ۱ب |
| ارز روی Document / Bank / Cash / Check | ✅ | |
| پرداخت بینارزی | ❌ مسدود | PAYMENT_CURRENCY_MISMATCH |
| انتقال بینارزی | ❌ | |
| مبالغ پایه روی DocumentLine | ❌ | |
| مانده شخص per currency | ❌ | |
| سند تسعیر پایان دوره | ❌ | فقط برچسب حساب در CoA |
| تست automated MC | ❌ تقریباً |
۲. تحلیل حیاتی: فعالسازی چندارزی روی خطوط سند و دادهٔ قبلی
این بخش پاسخ مستقیم به سؤال: اگر روی خطوط سند چندارزی را فعال کنیم، برای اطلاعات قبلی چه میشود؟
۲.۱ مدل فعلی خطوط
امروز document_lines فقط دارد:
debit,credit→ مبلغ به ارز سند (documents.currency_id)- بدون
currency_idخط - بدون
exchange_rate - بدون
debit_base/credit_base - بدون
currency_amountجدا
نرخ فاکتور (در صورت وجود) فقط در documents.extra_info.fx است، نه روی خط.
۲.۲ گزینههای طراحی خط چندارزی
گزینه A — ستونهای اختیاری روی خط (پیشنهاد اجرایی این سند)
افزودن ستونهای nullable:
| ستون | معنی |
|---|---|
currency_id |
ارز مبلغ خط (پیشفرض = ارز سند) |
exchange_rate |
نرخ قفلشده نسبت به ارز پایه در لحظه ثبت |
debit_base |
بدهکار به ارز پایه |
credit_base |
بستانکار به ارز پایه |
یا معادل فشردهتر:
| ستون | معنی |
|---|---|
exchange_rate |
نرخ سند/خط به پایه |
debit_base / credit_base |
مبالغ پایه |
قانون سازگاری عقبرو: ستونها nullable باشند؛ کد خواندن همیشه fallback داشته باشد.
گزینه B — فقط JSON در document_lines.extra_info
سریعتر، ولی برای گزارش/ایندکس/بستن سال ضعیفتر است. برای هسته حسابداری توصیه نمیشود.
گزینه C — بدون persist پایه؛ فقط convert-on-read (وضعیت نزدیک به امروز)
برای گزارش لحظهای کافی است، برای audit، بستن سال، و تسویه بینارزی کافی نیست.
تصمیم پیشنهادی سند: گزینه A با nullable + backfill تدریجی.
۲.۳ چه بر سر دادهٔ قبلی میآید؟
| نوع داده تاریخی | نیاز به تغییر اجباری؟ | رفتار پیشنهادی |
|---|---|---|
| کسبوکارهای تکارزی (اکثریت) | خیر | ستونهای جدید NULL؛ در runtime: rate=1 و *_base = debit/credit |
اسناد ارزی با extra_info.fx |
اختیاری ولی توصیهشده | backfill: exchange_rate و *_base از fx.rate |
اسناد ارزی بدون fx |
اختیاری با ریسک | backfill با resolve_rate_to_base(as_of=document_date) یا علامت fx_inferred=true |
| اسناد پایه (currency = default) | خیر | NULL یا پر کردن با rate=1 در backfill سبک |
| سالهای بستهشده | حساس | یا دستنخورده بمانند (خواندن با fallback) یا backfill فقطخواندنی بدون تغییر مانده منطقی |
۲.۴ آیا دادهٔ قبلی «خراب» میشود؟
اگر درست پیاده شود: خیر.
شرایط ایمنی:
- ستونهای جدید nullable باشند و migration فقط ADD COLUMN باشد (بدون NOT NULL فوری).
- همه خوانندهها این اولویت را رعایت کنند:
اگر debit_base/credit_base موجود → استفاده کن
وگرنه اگر document.extra_info.fx.rate موجود → amount × rate
وگرنه اگر rate book as-of موجود → amount × resolved_rate
وگرنه اگر ارز سند = ارز پایه → amount
وگرنه → سیاست when_no_rate (block در ثبت جدید؛ برای تاریخ: flag + rate=1 فقط با لاگ هشدار)
- نوشتههای جدید (پس از فاز مربوطه) همیشه base را persist کنند.
- هیچ jobی مانده سال بستهشده را دوباره «بازمحاسبه و بازنویسی سند بستن» نکند مگر با ابزار صریح ادمین.
۲.۵ آیا backfill اجباری است؟
| سناریو | اجباری؟ | توضیح |
|---|---|---|
| فقط افزودن ستون برای آینده | خیر | سیستم با fallback کار میکند |
| گزارش dual دقیق تاریخی | تقریباً بله | بدون backfill، نرخ ممکن است بعداً عوض شده باشد و معادل پایه «بازسازیشده» با snapshot تاریخی یکی نباشد |
| تسویه بینارزی روی فاکتورهای قدیمی | بله برای همان فاکتورها | بهتر است قبل از تسویه، fx خط/سند قفل شود |
| کسبوکارهای تکارزی | خیر | هیچ backfill سنگینی لازم نیست |
۲.۶ استراتژی مهاجرت پیشنهادی (۳ لایه)
لایه 1 — Schema only (ایمن، سریع)
ADD nullable columns + indexes
هیچ UPDATE انبوهی نیست
لایه 2 — Soft backfill (پسزمینه، idempotent)
برای اسنادی که extra_info.fx دارند:
line.exchange_rate = fx.rate
line.debit_base = debit * rate
line.credit_base = credit * rate
برای اسناد ارز پایه:
اختیاری: rate=1 و base=amount یا NULL بماند
لایه 3 — Inferred backfill (اختیاری، پرچمدار)
اسناد ارزی بدون fx:
resolve از rate book
developer_data/extra_info: { "fx_source": "inferred_backfill", ... }
گزارشها بتوانند «نرخ استنباطی» را متمایز کنند
۲.۷ ریسکهای دادهٔ قبلی (باید در QA پوشش داده شود)
- تغییر نرخ بعد از ثبت سند → بدون snapshot، گزارش تاریخی عوض میشود. به همین دلیل persist پایه مهم است.
- fallback نرخ ۱ در کد فعلی مانده شخص/بستن سال → پس از فعالسازی خط، باید برای دادهٔ جدید ممنوع و برای قدیم قابلردیابی باشد.
- سال بستهشده → backfill نباید سند افتتاحیه سال بعد را جابهجا کند.
- اسناد ترکیبی آینده (پرداخت بینارزی) → مدل «یک ارز برای کل سند» ممکن است کافی نباشد؛ فاز ۳ باید تصمیم D4/D1 را قطعی کند قبل از schema نهایی.
۲.۸ حکم اجرایی برای خطوط سند
فعالسازی چندارزی روی خطوط نیازمند تغییر اجباری و دستی دادهٔ قبلی برای تکارزی نیست.
برای چندارزی تاریخی، مهاجرت نرم (لایه ۱+۲) کافی و توصیهشده است.
بازطراحی اجباری همه اسناد قدیمی لازم نیست، به شرط وجود fallback یکسان در سرویسهای گزارش/مانده/بستن سال.
۳. اصل Progressive Disclosure (الزام هر فاز)
۳.۱ قوانین UI وقتی MC=OFF
| محل | رفتار اجباری |
|---|---|
| Toolbar | بدون chip نرخ |
| منوی تنظیمات | بدون تسعیر/نرخ/auto-sync (یا غیرفعال با توضیح) |
| فاکتور فروش/خرید | بدون انتخابگر ارز، بدون فیلد نرخ، بدون جمع دوگانه |
| پرداخت فاکتور | بدون ارز دوم / نرخ تبدیل |
| دریافت و پرداخت | بدون UI تبدیل |
| انتقال | بدون نرخ بینارزی |
| اشخاص | بدون کارت مانده per currency |
| اسناد/گزارشها | بدون فیلتر ارز و بدون «همه ارزها» |
| چاپ فاکتور | بدون بلوک معادل پایه |
| کالا | بدون بخش قیمت ارزی/sync نرخ |
۳.۲ قوانین Backend وقتی MC=OFF
- APIهای فقط-MC →
400 MULTI_CURRENCY_REQUIREDیا no-op امن طبق قرارداد endpoint currency_idاسناد =default_currency_id- تلاش برای پرداخت با ارز ≠ فاکتور → رد (مثل امروز)
- ستونهای base روی خط اگر پر شوند باید با rate=1 و ارز پایه سازگار باشند (دفاع در عمق)
۳.۳ چکلیست اجباری قبل از merge هر PR
- تست دستی session با کسبوکار تکارزی
- تست API تکارزی برای endpointهای جدید
- عدم نشت ویجت پشت
MultiCurrencyGate - عدم تغییر رفتار مسیر همارز موجود
۴. نقشه فازها (خلاصه اجرایی)
| فاز | عنوان | پیشنیاز | وضعیت تقریبی | اولویت انتشار |
|---|---|---|---|---|
| P0 | Gate + قرارداد MC | — | ✅ v1 | — |
| P1 | نرخ روز دستی + toolbar | P0 | ✅ v1 | — |
| P1b | auto-sync نرخ | P1 | ✅ v1 | — |
| P2 | جمع دوگانه فاکتور + نمایش نرخ | P1 | ✅ v1 | — |
| P2.5 | ستونهای پایه روی خط + مهاجرت نرم | P2 | ✅ v1 | — |
| P3 | پرداخت/دریافت بینارزی + اختلاف نرخ | P2.5 + D1 | ✅ هسته v1 | — |
| P4 | انتقال بینارزی | P2.5 + D4 | ✅ هسته API v1 | — |
| P5 | مانده شخص per currency | P1 | ✅ v1 | — |
| P6 | قیمت کالا دوگانه + sync نرخ | P1 | ✅ v1 | — |
| P7 | یکدستسازی گزارشها/اسناد | P2.5 | ✅ جزئی v1 | تکمیل در v2 |
| P8 | تسعیر پایان دوره | P2.5 + P7 | ✅ محدود v1 | گسترش در v2 |
| P9 | تست، مشاهدهپذیری، سختسازی | همهفازها | ⬜ مستمر | v1+v2 |
ترتیب انتشار پیشنهادی:
P2 → P2.5 → P5 → P3 → P7 → P4 → P6 → P8
P9 در هر PR
۵. فازها بهصورت دقیق
فاز P2 — جمع دوگانه فاکتور (ارزی + پایه + نرخ)
هدف: کاربر چندارزی در ثبت/نمایش/چاپ فاکتور ارزی، جمع ارز، معادل پایه و نرخ را ببیند.
تکارزی: هیچ بلوک جدیدی نباشد.
دامنه
- فروش و خرید و برگشتیها روی مسیر invoice
- فقط وقتی
MC=ONوdocument.currency_id ≠ default_currency_id
Backend
- در پاسخ جزئیات فاکتور فیلدهای محاسبهشده پایدار:
totals.foreigntotals.basefx(از snapshot؛ اگر نبود از resolve — ترجیح: snapshot اجباری بماند)
- اطمینان از پر شدن
extra_info.fxدر create/update همه زیرنوعهای invoice موردنیاز - تست: تکارزی → بدون الزام فیلدهای dual؛ چندارزی ارزی → fx و totals.base موجود
Frontend
- بلوک جمع دوگانه در فرم فاکتور (پشت Gate)
- چاپ و لینک عمومی همان منطق
- عدم نمایش وقتی ارز = پایه یا MC=OFF
معیار پذیرش
- MC=OFF: UI فاکتور بدون تغییر ظاهری FX
- MC=ON + فاکتور USD:
1,000 USD+ معادل پایه + نرخ - ویرایش فاکتور نرخ snapshot را حفظ/بهروز میکند طبق سیاست
ریسک
نمایش تبدیل بدون persist پایه همچنان به snapshot هدر وابسته است — با P2.5 کامل میشود.
تخمین نسبی
کوچک تا متوسط (عمدتاً UI + تقویت response)
فاز P2.5 — ستونهای چندارزی روی خط سند + مهاجرت نرم
هدف: زیرساخت persist مبالغ پایه بدون شکستن دادهٔ قبلی و بدون تأثیر ظاهری روی تکارزی.
تصمیم Schema (پیشنهاد قفلشونده قبل از کد)
document_lines:
exchange_rate Numeric(24,10) NULL
debit_base Numeric(24, 6 یا 2) NULL
credit_base Numeric(24, 6 یا 2) NULL
# اختیاری اگر خط بتواند ارزی غیر از سند داشته باشد (برای P3):
# currency_id INT NULL FK currencies
توصیه این سند: در P2.5 ابتدا
exchange_rate+debit_base+credit_baseاضافه شود.
line.currency_idفقط اگر در طراحی P3 اثبات شد که یک سند چندارزِ خطی لازم است؛ وگرنه ارز همانdocument.currency_idبماند.
Backend
- Migration لایه ۱ (nullable only)
- Helper واحد:
line_amounts_to_base(document, line) -> (debit_base, credit_base, rate, source) - در create/update اسناد (حداقل invoice؛ سپس receipts/transfers/expenses):
- اگر ارز سند = پایه → میتوان NULL گذاشت یا rate=1 پر کرد (ترجیح: برای سادگی گزارش، rate=1 و base=amount)
- اگر ارزی → از
extra_info.fx.rateپر کردن اجباری
- Job backfill لایه ۲ (idempotent، batch، قابل توقف)
- خواندن گزارش/مانده/year-end از helper واحد (جایگزینی تدریجی
_person_line_amount_to_base)
تأثیر دادهٔ قبلی (خلاصه عملیاتی)
| کسبوکار | اقدام |
|---|---|
| تکارزی | هیچ؛ ستون NULL؛ رفتار عین قبل |
| چندارزی با fx روی فاکتور | backfill لایه ۲ توصیهشده |
| چندارزی بدون fx | لایه ۳ اختیاری + هشدار در گزارش |
Frontend
- در این فاز الزامی به UI جدید نیست (مگر نمایش debug/ادمین اختیاری پشت Gate)
- تکارزی هیچ تفاوتی نمیبیند
معیار پذیرش
- Migration روی DB بزرگ بدون downtime طولانی (ADD COLUMN)
- اسناد قدیمی بدون backfill همچنان خوانده میشوند
- فاکتور جدید ارزی خطوط با base پر میشوند
- مانده شخص قبل/بعد backfill برای نمونه طلایی اختلاف غیرمنتظره ندارد (تلورانس گرد کردن)
- MC=OFF: هیچ تغییر UX
ریسکها و کنترل
- اختلاف اعشار: استفاده از
currency_quant - سال بستهشده: backfill فقط خطوط؛ بازتولید سند بستن ممنوع مگر ابزار جدا
- عملکرد: ایندکس لازم نیست روی همه ستونهای مبلغ؛ فیلتر همچنان روی
documents.currency_id
تخمین نسبی
متوسط تا بزرگ (schema + سرویس مرکزی + backfill + جایگزینی خوانندهها)
فاز P3 — پرداخت و دریافت بینارزی (هسته عملیاتی)
هدف: فاکتور دلاری، پرداخت ریالی (یا بالعکس) با نرخ per قسط؛ ثبت حسابداری صحیح.
پیشنیاز تصمیم محصول (باید قبل از کد قفل شود)
| کد | سؤال | گزینهها | پیشنهاد سند |
|---|---|---|---|
| D1 | اختلاف نرخ پرداخت vs نرخ فاکتور؟ | ثبت GL سود/زیان تسعیر / فقط اطلاعاتی | ثبت GL اگر حساب تسعیر در CoA کسبوکار فعال است؛ وگرنه informational + هشدار |
| D2 | واحد نمایش نرخ | ریال / تومان نمایشی | برچسب از ارز پایه؛ تبدیل تومان فقط UI اگر تنظیم باشد |
| D6 | حذف آخرین ارز فرعی | قفل اگر سند/حساب/کالای ارزی هست | بله قفل (V2-P7) |
مدل داده آیتم پرداخت (پیشنهاد)
{
"transaction_type": "bank",
"bank_id": 10,
"amount": 280000000,
"amount_currency_id": 1,
"settles_amount": 200,
"settles_currency_id": 2,
"fx": {
"rate": "1400000",
"mode": "manual",
"base_currency_id": 1,
"from_currency_id": 2,
"to_currency_id": 1
}
}
معنی حسابداری:
- بدهی شخص به اندازه
settles_amountبه ارز فاکتور کم میشود - حساب بانک/صندوق به اندازه
amountبه ارز حساب جابهجا میشود - اختلاف طبق D1 ثبت یا فقط نمایش داده میشود
Backend
- شل کردن
_validate_invoice_payment_item_currencyفقط وقتی:MC=ON- و فیلدهای settles/fx معتبر
- مسیر همارز فعلی دستنخورده بماند
- تولید خطوط سند با base پرشده (وابسته به P2.5)
- ذخیره fx per payment item در
extra_info - همان الگو برای
receipt_payment_service(اقساط/تهاتر) - خطاهای راهنما به فارسی وقتی نرخ وارد نشده
Frontend
- در ویجت پرداخت فاکتور: اگر حساب ≠ ارز فاکتور و MC=ON → فیلد نرخ + مبلغ تسویه ارزی
- لیست پرداختها با نمایش دوگانه
- MC=OFF: صفر فیلد جدید
معیار پذیرش
- سناریو: فاکتور 1000 USD، دو پرداخت ریالی با دو نرخ مختلف
- مانده فاکتور به USD درست کم شود
- موجودی حساب ریالی درست کم/زیاد شود
- تکارزی: رفتار پرداخت عین قبل
- تلاش API تکارزی برای cross-currency → رد امن
وابستگی دادهٔ قبلی
فاکتورهای قدیمی بدون fx قبل از اولین تسویه بینارزی باید fx بگیرند (resolve یا ورود دستی)، وگرنه تسویه مسدود با پیام واضح.
تخمین نسبی
بزرگ (بحرانیترین فاز محصول)
فاز P4 — انتقال بانکی/صندوق بینارزی
هدف: انتقال از حساب دلاری به ریالی با نرخ دستی.
تصمیم D4 (قبل از کد)
ارز سند انتقال:
- پیشنهادی: سند با ارز پایه + دو مبلغ در extra، یا
- سند بدون فرض تکمبلغ و خطوط هرکدام منطبق با ارز حساب مبدأ/مقصد + base
Backend / Frontend
- فقط وقتی ارز مبدأ ≠ مقصد و MC=ON
- همارز: فرم فعلی بدون تغییر
معیار پذیرش
- موجودی دو حساب درست
- MC=OFF بدون UI نرخ
تخمین نسبی
متوسط
فاز P5 — مانده اشخاص به تفکیک ارز
هدف: نمایش بدهی/بستانکاری per currency + معادل پایه.
Backend
GET .../persons/{id}/balances-by-currency- تجمیع بدون تبدیل per
document.currency_id base_equivalentاختیاری با نرخ latest یا as-of
Frontend
- کارت خلاصه فقط MC=ON
- لیست اشخاص: برای تکارزی همان یک عدد
معیار پذیرش
- تکارزی بدون کارت جدید
- چندارزی: IRR و USD جدا + جمع معادل شفاف
نکته دادهٔ قبلی
نیاز به تغییر schema خط ندارد؛ از document.currency_id فعلی کار میکند. دقت معادل پایه با P2.5 بهتر میشود.
تخمین نسبی
کوچک تا متوسط — مناسب انتشار زود هنگام برای ارزش کاربر
فاز P6 — قیمت دوگانه کالا + sync از نرخ
هدف: قیمت خرید/فروش پایه و ارزی؛ آپدیت اختیاری از نرخ روز.
تصمیم D3
- A: فیلد روی Product (UX سادهتر)
- B: فقط PriceList موجود
پیشنهاد: A برای فرم کالا + همگام اختیاری با لیستقیمت پیشفرض.
معیار پذیرش
- MC=OFF فقط قیمت فعلی
- MC=ON دو قیمت + دکمه بروزرسانی از نرخ
تخمین نسبی
متوسط
فاز P7 — یکدستسازی اسناد و گزارشها
هدف: فیلتر ارز یکسان + حالت «همه ارزها» با تسعیر شفاف.
گزارشها
| گزارش | رفتار هدف |
|---|---|
| تراز آزمایشی | فیلتر ارز؛ در «همه» جمع از *_base |
| دفتر کل / روزنامه | فیلتر + نمایش مبلغ اصلی کنار پایه در «همه» |
| ترازنامه | مانند TB از base |
| سود و زیان | یکسان با موتور base واحد |
| گردش حساب | دوگانه در MC |
قوانین
- موتور تبدیل = همان helper P2.5
- ممنوع جمع خام چند ارز بدون تبدیل
- MC=OFF: بدون dropdown
معیار پذیرش
- نمونه طلایی چند سند USD/IRR در TB «همه» با جمع پایه صحیح
- تکارزی بدون فیلتر ارز
تخمین نسبی
متوسط تا بزرگ
فاز P8 — تسعیر پایان دوره (اختیاری / تصمیم محصول)
هدف: سند تعدیلی سود/زیان تسعیر تحققنیافته برای ماندههای ارزی باز.
پیشنیاز
- حسابهای درآمد/هزینه تسعیر در CoA (برچسبها امروز هستند)
- مانده per currency (P5) و base lines (P2.5)
- سیاست: کدام حسابها تسعیر میشوند (پولی: بانک، شخص، چک؛ غیرپولی: موجودی؟)
خارج از محدوده اولیه اگر محصول نخواست
میتوان P8 را به نسخه بعدی موکول کرد؛ بستن سال فعلی فعلاً به پایه collapse میکند.
معیار پذیرش
- تولید سند تسعیر قابلبازبینی
- MC=OFF: منو دیده نشود
- سال بستهشده بدون ابزار ادمین تغییر نکند
تخمین نسبی
بزرگ — فقط پس از پایدار شدن P3/P7
فاز P9 — تست، مشاهدهپذیری، سختسازی (مستمر)
حداقل پوشش هر فاز:
- API تکارزی (باید امن/مخفی)
- API چندارزی happy path
- رگرسیون پرداخت همارز
- گرد کردن اعشار
- golden test برای Gate در Flutter (در صورت امکان)
- لاگ نرخ و currency ids در مسیرهای حساس
ابزار QA داده:
- اسکریپت مقایسه مانده شخص قبل/بعد backfill
- گزارش اسناد ارزی بدون fx
۶. ماتریس تأثیر دادهٔ قبلی به تفکیک فاز
| فاز | آیا داده قدیمی باید عوض شود؟ | اگر نشود چه میشود؟ |
|---|---|---|
| P2 | خیر | نمایش از snapshot/resolve |
| P2.5 | توصیه به backfill لایه ۲ | fallback خواندن؛ گزارش تاریخی کمی ناپایدارتر |
| P3 | فقط فاکتورهای هدف تسویه بینارزی باید fx داشته باشند | تسویه مسدود با پیام |
| P4 | خیر اجباری | — |
| P5 | خیر | کار میکند؛ معادل پایه با resolve |
| P6 | خیر | — |
| P7 | بهتر است P2.5 انجام شده باشد | حالت «همه» دقیق نیست |
| P8 | بله برای کیفیت | تسعیر پایان دوره قابل اعتماد نیست |
جمعبندی: فعالسازی خط چندارزی = schema سازگار با گذشته؛ نه بازنویسی اجباری تاریخچه تکارزی.
۷. تصمیمهای باز محصول (باید قبل از فاز مربوطه بسته شوند)
| کد | موضوع | فاز | وضعیت |
|---|---|---|---|
| D1 | ثبت حسابداری سود/زیان تسعیر در پرداخت؟ | P3 | ✅ v1 — ثبت GL در 60204/70801 |
| D2 | نمایش نرخ ریال یا تومان؟ | P2/P3 | کد شد (V2-P7) — rate_display_unit |
| D3 | قیمت کالا روی Product یا PriceList؟ | P6 | ✅ v1 — گزینه A (فیلد روی Product) |
| D4 | مدل ارز سند انتقال بینارزی | P4 | ✅ v1 — سند پایه + account_currency_amount |
| D5 | Provider دوم (Mesghal و …) | اختیاری | کد شد (V2-P7) — mesghal JSON/Tala |
| D6 | قفل حذف آخرین ارز فرعی اگر سند ارزی هست | P0/P3 | کد شد (V2-P7) — usage گسترده + قفل حذف |
| D7 | آیا line.currency_id لازم است؟ |
P2.5/P3 | ✅ v1 بدون آن؛ بازنگری v2 اگر USD↔EUR لازم شود |
| D8 | تسعیر پایان دوره در نسخه جاری؟ | P8 | ✅ v1 — شipped محدود (بانک/صندوق/تنخواه) |
| D9 | backfill لایه ۳ (inferred) پیشفرض روشن؟ | P2.5 | پیشنهاد: خاموش؛ دستی ادمین (اختیاری v2) |
پس از تصمیم، در همین جدول وضعیت را بهروز کنید.
۸. برنامه انتشار پیشنهادی (Release Train)
موج ۱ — ارزش سریع بدون ریسک داده
- P2 نمایش دوگانه فاکتور
- P5 مانده شخص per currency
موج ۲ — زیرساخت حسابداری
- P2.5 ستون پایه + helper واحد + backfill لایه ۲
- P7 یکدستسازی گزارشها روی helper جدید
موج ۳ — عملیاتی
- P3 پرداخت/دریافت بینارزی (پس از D1)
- P4 انتقال بینارزی (پس از D4)
موج ۴ — تکمیل
- P6 قیمت کالا
- P8 تسعیر پایان دوره (اگر D8 تأیید شد)
- سختسازی P9 و پاکسازی fallback نرخ ۱ در مسیرهای حیاتی
هر موج فقط وقتی merge میشود که چکلیست تکارزی سبز باشد.
۹. معیارهای پذیرش سراسری (هر موج)
۹.۱ کسبوکار تکارزی
- هیچ منو/فیلد/فیلتر FX جدید
- ماندهها و گزارشها عدد قبلی را حفظ میکنند (تلورانس گرد کردن صفر برای ارز بدون اعشار)
- APIهای جدید MC برای این کسبوکار امناند
۹.۲ کسبوکار چندارزی
- نرخ و ارز در تراکنشهای جدید snapshot میشوند
- گزارش «یک ارز» و «همه» معنای شفاف دارند
- پرداخت همارز رگرسیون ندارد
۹.۳ داده تاریخی
- اسناد قدیمی بدون crash خوانده میشوند
- backfill قابل تکرار و قابل گزارش است
- سال بستهشده بدون ابزار صریح تغییر نمیکند
۱۰. خارج از محدوده این برنامه اجرایی
- چند ارز پایه همزمان برای یک کسبوکار
- رمزارز / نرخ لحظهای معاملات
- تغییر قوانین سامانه مودیان برای فاکتور غیرریالی (مسیر قانونی جدا)
- اجازه تغییر ارز پایه پس از شروع عملیات بدون پروژه مهاجرت مستقل
۱۱. نحوه پیشبرد کار روی این سند
- تصمیمهای باز موردنیاز موج بعدی را قفل کنید.
- برای هر فاز issue/PR با عنوان
MC-Exec-P#: ...و لینک به بخش همین فایل باز کنید. - قبل از شروع P2.5، این سند را با D7 (لزوم
line.currency_id) بهروز کنید. - پس از اتمام هر فاز، چکباکسها و وضعیت جدول بخش ۴ را بهروز کنید و commit/PR را ثبت کنید.
- تا وقتی چکلیست Progressive Disclosure سبز نشده، فاز بعدی منتشر نشود.
۱۲. جمعبندی یکصفحهای برای مدیریت
- امروز حسابیکس چندارزی سندمحور + نرخ دارد؛ چندارزی عملیاتی ناقص است.
- فعالسازی مبالغ پایه روی خطوط سند داده تکارزی را نمیشکند اگر ستونها nullable و خواندن با fallback باشد.
- داده چندارزی قدیمی بهتر است با backfill از
extra_info.fxغنی شود؛ اجباری برای روشنکردن سیستم نیست، برای دقت تاریخی و تسویه بینارزی هست. - ترتیب درست اجرا: نمایش دوگانه → persist پایه → مانده per ارز → پرداخت بینارزی → گزارش یکدست → انتقال → قیمت → تسعیر پایان دوره.
- در تمام فازها، کسبوکار تکارزی پشت Gate میماند و جریان کاریاش نباید تغییر کند.
۱۴. وضعیت اجرای موج ۱–۲ (بهروز ۲۰۲۶-۰۷-۲۵)
بکاپ قبل از اجرا: /opt/hesabix/backups/hesabix_pre_mc_exec_20260725_155942.dump
| فاز | وضعیت | شرح |
|---|---|---|
| P2 | ✅ | fx_totals در پاسخ فاکتور؛ بنر جمع دوگانه؛ Gate |
| P2.5 | ✅ | ستونهای پایه روی خطوط؛ backfill؛ helper واحد |
۱۵. وضعیت اجرای موج ۳ (بهروز ۲۰۲۶-۰۷-۲۵)
بکاپ: /opt/hesabix/backups/hesabix_pre_mc_p5p7_*.dump
| فاز | وضعیت | شرح |
|---|---|---|
| P5 | ✅ | GET /persons/{id}/balances-by-currency + کارت UI پشت Gate در جزئیات شخص |
| P7 | ✅ جزئی | تراز/مانده حساب وقتی فیلتر ارز نباشد از debit_base/credit_base جمع میزند |
| P3 | ✅ هسته | پرداخت بینارزی وقتی یکی از ارزها پایه است؛ settles_amount + نرخ؛ ثبت سود/زیان تسعیر 60204/70801؛ fx_settlement روی خط شخص |
| P4 | ✅ هسته | انتقال بینارزی با destination_amount/fx_rate؛ مبالغ بومی در account_currency_amount؛ موجودی بانک بومیمحور |
محدودیتهای آگاهانه نسخه ۱
- پرداخت/انتقال فقط وقتی یکی از دو ارز = ارز پایه باشد
- تسعیر پایان دوره فعلاً برای حسابهای پولی بانک/صندوق/تنخواه (نه شخص/چک)
تکمیل موج ۴ (۲۰۲۶-۰۷-۲۵)
| مورد | وضعیت |
|---|---|
موجودی صندوق/تنخواه با منطق بومی account_currency_amount |
✅ |
UI پرداخت بینارزی (settles_amount + fx_rate) پشت Gate |
✅ |
| P6 قیمت دوگانه کالا + sync از نرخ | ✅ |
| P8 تسعیر پایان دوره (پیشنمایش + سند دستی) | ✅ |
بکاپ موج ۴: /opt/hesabix/backups/hesabix_pre_mc_v1fix_*.dump
اصل تکارزی
بدون ارز فرعی، مسیرهای بینارزی مسدود میمانند و UI چندارزی دیده نمیشود.
۱۶. جمعبندی نسخه ۱ (مرز محصول)
نسخه ۱ چندارزی عملیاتی پایه را تحویل داد: Gate، نرخ، جمع دوگانه فاکتور، مبالغ پایه روی خط، مانده شخص per ارز، پرداخت/انتقال وقتی یکی از ارزها پایه است، قیمت ارزی کالا، تسعیر پایان دوره برای حسابهای نقدی، و جمع پایه در تراز وقتی فیلتر ارز نیست.
مرز آگاهانه v1 (ورودی مستقیم v2):
| # | محدودیت v1 |
|---|---|
| L1 | |
| L2 | |
| L3 | |
| L4 | *_base |
| L5 | |
| L6 | |
| L7 | |
| L8 |
۱۷. برنامه اجرایی نسخه ۲ (MC-v2)
وضعیت سند: اجرایی — ۲۰۲۶-۰۷-۲۵
پیشنیاز: پایدار بودن v1 در محیط واقعی + بکاپ قبل از هر موج
اصل طلایی: Progressive Disclosure برای تکارزی بدون تغییر؛ هیچ فاز v2 نباید MC=OFF را شلوغ کند.
پیشرفت موج فعلی: V2-P0…P7 ✅ (شامل PriceList sync، D5 mesghal، D2 نمایش، D6) · V2-P8 جزئی ✅ (health/backfill admin + stamp بدون نرخ ۱) · باقی V2-P8 کاملتر
۱۷.۱ هدف نسخه ۲
رساندن محصول از «چندارزی عملیاتی پایه (با قید ارز پایه)» به «چندارزی عملیاتی کاملتر برای کسبوکارهای چندارز واقعی»:
- بستن شکافهای UI/گزارش v1 که API دارد ولی کاربر نمیبیند
- تسویه و انتقال فرعی↔فرعی (مثلاً USD↔EUR) با مدل نرخ شفاف
- تسعیر پایان دوره برای ماندههای پولی کاملتر (شخص، چک)
- یکدستسازی گزارشها و دفاع در عمق Gate
- سختسازی داده و قفلهای ایمنی ارز
۱۷.۲ نقشه فازهای v2
| فاز | عنوان | اولویت | وابستگی | تخمین نسبی |
|---|---|---|---|---|
| V2-P0 ✅ | تکمیل UI انتقال بینارزی + Gate گزارشها | فوری | L2, L5 | کوچک |
| V2-P1 ✅ | گزارشها «همه ارزها» = پایه + نمایش دوگانه | بالا | L4, P7 | متوسط |
| V2-P2 ✅ | تسعیر پایان دوره برای شخص (+ چک) | بالا | L3, P5, P8 | متوسط–بزرگ |
| V2-P3 ✅ | پرداخت/انتقال فرعی↔فرعی (برداشتن BASE_REQUIRED) | بالا | L1, تصمیم E1/E2 | بزرگ |
| V2-P4 | دریافت/پرداخت مستقل بینارزی | متوسط | V2-P0/P3 | متوسط |
| V2-P5 ✅ | چاپ/لینک عمومی + کشفپذیری مانده ارز در لیست اشخاص | متوسط | P2, P5 | کوچک |
| V2-P6 | قیمت کالا: دکمه sync فوری + همگام PriceList | متوسط | P6 | کوچک–متوسط |
| V2-P7 | ایمنی ارز (D6) + Provider دوم (D5) + نمایش واحد (D2) | متوسط | — | کوچک–متوسط |
| V2-P8 | سختسازی P9، backfill inferred اختیاری، تست طلایی | مستمر | همه | متوسط |
ترتیب انتشار پیشنهادی:
V2-P0 → V2-P1 → V2-P2 → V2-P5
↘ V2-P3 (پس از E1/E2) → V2-P4
V2-P6 / V2-P7 موازی نسبی
V2-P8 در هر PR
۱۷.۳ فازها بهتفصیل
V2-P0 — تکمیل شکاف UI انتقال + Gate گزارش (سریعترین ارزش)
هدف: قابلیتی که backend v1 دارد، در Flutter قابل استفاده شود؛ تکارزی فیلتر ارز نبیند.
کارها
- فرم انتقال: حذف فیلتر اجباری همارزی وقتی
isMultiCurrency - فیلدهای
destination_amountوfx_rateوقتی ارز مبدأ ≠ مقصد - اعتبارسنجی همان قید v1 تا قبل از V2-P3 (یکی پایه باشد) با پیام واضح
- گزارشهای TB / روزنامه / سودوزیان / ترازنامه: مخفیسازی dropdown ارز وقتی MC=OFF
- تست رگرسیون انتقال همارز
معیار پذیرش
- MC=ON: انتقال USD→IRR از UI بدون API دستی
- MC=OFF: هیچ فیلتر ارز در گزارشهای هدف
V2-P1 — یکدستسازی گزارشها (تکمیل P7)
هدف: در حالت «همه ارزها» هیچجا جمع خام چند ارز رخ ندهد.
کارها
- روزنامه / دفتر کل: در «همه» جمع از
debit_base/credit_base(+ fallback helper) - نمایش اختیاری مبلغ بومی کنار پایه در ردیفها (فرمت مشترک)
- ترازنامه و هر گزارش باقیمانده همان موتور (
account_balance_coreوقتیcurrency_id is None) - نمونه طلایی USD+IRR در TB و journal
معیار پذیرش
- گزارش «همه» برای کسبوکار نمونه با دو ارز = جمع پایه صحیح (± تلورانس گرد)
- فیلتر یک ارز = فقط مبالغ بومی همان ارز
V2-P2 — تسعیر پایان دوره: شخص و چک
هدف: سود/زیان تحققنیافته برای AR/AP ارزی و چکهای ارزی باز.
پیشنیاز محصول
- تأیید حسابهای 60204/70801 فعال در CoA کسبوکارها
- سیاست: آیا چک در محدوده پولی هست؟ (پیشنهاد: بله برای چکهای وصولنشده)
کارها
- گسترش
period_end_fx_revaluation_serviceبه مانده شخص per currency (غیرپایه) - خط تعدیل بدون تغییر مانده بومی ارز (همان الگوی
fx_settlement/account_currency_amount=0) - چکهای باز با
currency_id ≠ base(در صورت تأیید) - هشدار وقتی
book_baseناقص است (نیاز backfill) — بدون ایجاد تعدیل جعلی (adj=0 + warning) - UI پیشنمایش گروهبندیشده (نقدی / اشخاص / چک)
معیار پذیرش
- پیشنمایش شخص ارزی با adj غیرصفر قابل ایجاد سند
- مانده بومی ارز شخص پس از سند تغییر نکند؛ فقط ارزش پایه
ریسک جهت بدهکار/بستانکار شخص و نوع فاکتور؛ نیاز به تست طلایی چند سناریو.
V2-P3 — تسویه/انتقال فرعی↔فرعی (برداشتن قید پایه)
هدف: پرداخت فاکتور USD از حساب EUR (و انتقال مشابه) بدون اجبار یکیبودن با ارز پایه.
تصمیمهای قفلشونده قبل از کد
| کد | موضوع | گزینهها | پیشنهاد |
|---|---|---|---|
| E1 ✅ | مدل نرخ جفت ارز | (A) دو نرخ به پایه و ضرب متقاطع (B) نرخ مستقیم جفت در جدول جدید | قفل A — بدون schema نرخ جفت؛ سازگار با business_currency_rates |
| E2 ✅ | ارز سند حسابداری | (A) همیشه پایه (B) ارز پرداخت (C) line.currency_id |
قفل A مثل P4؛ سند بینارزی همیشه پایه |
| E3 ✅ | تلورانس و سود/زیان | GL تسعیر مثل v1 | ادامه GL روی 60204/70801 |
کارها
- بازنویسی
resolve_cross_currency_payment_plan/ transfer برای مسیر غیرپایه↔غیرپایه - محاسبه:
amount_base = foreign × rate_to_baseبرای هر طرف؛fx_diffروی پایه - UI: حذف قید پایه؛ پیشنهاد مقصد از نرخ متقاطع؛ نرخهای به پایه از جدول
- تست: USD invoice + EUR bank؛ انتقال USD→EUR
معیار پذیرش
- دیگر
CROSS_CURRENCY_BASE_REQUIREDبرای MC=ON در مسیرهای پوششدادهشده - سند متوازن به پایه؛ مانده بومی هر حساب درست
ریسک حسابداری: بالاترین فاز v2 — فقط پس از V2-P0 و نمونههای طلایی v1.
V2-P4 — دریافت/پرداخت مستقل بینارزی
هدف: اسناد receipt/payment خارج از فاکتور همان قابلیت تسویه بینارزی را داشته باشند.
کارها
- استفاده مجدد از helper تسویه در
receipt_payment_service - UI دیالوگ دریافت/پرداخت مشابه فاکتور
- لینک به فاکتور اختیاری +
settles_amountوقتی ارز فرق دارد
V2-P5 — چاپ، لینک عمومی، کشفپذیری
کارها
- بلوک جمع دوگانه در قالب چاپ و صفحه عمومی فاکتور (Gate)
- ستون/نشانگر ساده «چندارزی» یا ماندههای غیرپایه در لیست اشخاص (اختیاری، فشرده)
- دکمه «بروزرسانی از نرخ» در فرم کالا (علاوه بر auto) — جزئیات در V2-P6
V2-P6 — تکمیل قیمت کالا
کارها
- دکمه sync فوری در فرم کالا → API موجود
- همگام اختیاری با PriceList پیشفرض هنگام sync
- Gate صریح
MultiCurrencyGateبهجای فقطcurrencies.length > 1
V2-P7 — ایمنی و محصول جانبی
کارها
- D6: جلوگیری از حذف/غیرفعالسازی آخرین ارز فرعی وقتی سند/حساب با آن ارز وجود دارد
- D5: Provider دوم (mesghal بهصورت JSON عمومی / پیشفرض سازگار با api.tala.ir)
- D2: تنظیم نمایش ریال/تومان برای نرخ (
rate_display_unitدر سیاست تسعیر) - قفل تغییر
default_currency_idپس از وجود اسناد (خارج از محدوده تغییر پایه — فقط دفاع)
V2-P8 — سختسازی و مشاهدهپذیری
کارها
- تستهای integration برای مسیرهای V2-P0…P3 (پوشش تستهای
test_v2_*) - گزارش ادمین: اسناد ارزی بدون
fx/ خطوط بدون*_base(GET /admin/fx-providers/data-health/{business_id}) - ابزار اختیاری backfill inferred (D9) با dry-run (
POST .../backfill-base) - کاهش/حذف fallback نرخ ۱ در مسیر stamp خطوط (نوشتن) — خواندن گزارش هنوز با هشدار/fallback محافظهکارانه
۱۷.۴ ماتریس تأثیر داده در v2
| فاز | تغییر داده قدیمی؟ | اگر نشود |
|---|---|---|
| V2-P0 | خیر | — |
| V2-P1 | بهتر است backfill P2.5 کامل باشد | «همه» برای خطوط بدون base ناپایدارتر |
| V2-P2 | خیر اجباری؛ کیفیت وابسته به *_base |
هشدار needs_backfill |
| V2-P3 | خیر schema اجباری در پیشنهاد E1/E2-A | — |
| V2-P7 D6 | بله | قفل حذف با usage گسترده (اسناد، حساب، کالا) |
۱۷.۵ تصمیمهای باز مخصوص v2 (باید قبل از فاز قفل شوند)
| کد | موضوع | فاز | پیشنهاد |
|---|---|---|---|
| E1 ✅ | نرخ جفت فرعی↔فرعی | V2-P3 | متقاطع از دو نرخ به پایه — قفل شد |
| E2 ✅ | ارز سند بینارزی | V2-P3 | همیشه پایه — قفل شد |
| E3 ✅ | ادامه GL تسعیر | V2-P3 | بله — قفل شد |
| E4 ✅ | چک در تسعیر پایان دوره؟ | V2-P2 | بله برای چک باز |
| E5 | ایجاد تسعیر وقتی book_base ناقص؟ | V2-P2 | warning + adj=0 (بدون تعدیل جعلی) |
| D2 | ریال/تومان نمایشی | V2-P7 | کد شد — rate_display_unit در سیاست تسعیر |
| D5 | Provider دوم | V2-P7 | کد شد — mesghal (JSON عمومی / Tala-compatible) |
| D6 | قفل حذف ارز فرعی | V2-P7 | بله |
۱۷.۶ خارج از محدوده نسخه ۲ (همچنان)
همان بخش ۱۰ این سند، بهعلاوه:
- چند ارز پایه همزمان
- تغییر ارز پایه پس از عملیات (پروژه مهاجرت جدا)
- رمزارز / نرخ لحظهای معاملاتی
- قوانین مودیان برای فاکتور غیرریالی (مسیر قانونی جدا)
- حسابداری ارزی موجودی کالا (تسعیر غیرپولی) — مگر تصمیم محصول صریح
۱۷.۷ معیار پذیرش سراسری v2
تکارزی: بدون تغییر نسبت به پس از v1.
چندارزی: مسیرهای L1–L8 یا بسته شدهاند یا با تصمیم محصول صریحاً خارج ماندهاند.
حسابداری: هر سند بینارزی متوازن است؛ مانده بومی حساب/شخص خراب نمیشود.
۱۷.۸ چکلیست شروع کار v2
- بکاپ DB قبل از موج اول v2
- قفل E1/E2 قبل از شروع V2-P3
- قفل E4/E5 قبل از V2-P2
- issue/PR با پیشوند
MC-v2-P# - پس از هر فاز، همین بخش ۱۷ را بهروز کنید
۱۸. جمعبندی یکصفحهای برای مدیریت (v2)
- v1 چندارزی عملیاتی را با قید «یکی از ارزها باید پایه باشد» و تسعیر نقدی تحویل داد.
- v2 باید اول UI/گزارشهای نصفه را ببندد، بعد تسعیر اشخاص، بعد فرعی↔فرعی.
- پرریسکترین کار: V2-P3 (USD↔EUR) — بدون قفل E1/E2 شروع نشود.
- تکارزی همچنان پشت Gate میماند.