arc/docs/MULTI_CURRENCY_EXECUTION_PLAN.md

49 KiB
Raw Permalink Blame History

سند اجرایی فازبه‌فاز چندارزی عملیاتی — حسابیکس

نوع سند: برنامه اجرایی (Execution Plan)
وضعیت: پیش‌نویس اجرایی — بدون اعمال کد در این مرحله
مخاطب: مالک محصول، معمار، Backend، Flutter، QA
مرجع وضعیت فعلی: گزارش تحلیل چندارزی + MULTI_CURRENCY_IMPLEMENTATION_GUIDE.md
اصل غیرقابل‌مذاکره: کسب‌وکار تک‌ارزی نباید هیچ UI/جریان کاری چندارزی غیرضروری ببیند.


۰. هدف و تعریف موفقیت

۰.۱ هدف نهایی

رساندن حسابیکس از «چندارزی سطح سند + نرخ + گزارش محدود» به «چندارزی عملیاتی قابل‌اعتماد» با این قابلیت‌ها:

  1. فاکتور/خرید/دریافت/پرداخت/انتقال با ارز مناسب
  2. تسویه بین‌ارزی با نرخ تراکنش
  3. مانده طرف‌حساب به تفکیک ارز (+ معادل پایه)
  4. گزارش‌های یکدست (فیلتر ارز / همه ارزها با تسعیر شفاف)
  5. تسعیر پایان دوره (در صورت تصمیم محصول)
  6. رفتار صفرِ اصطکاک برای کسب‌وکار تک‌ارزی

۰.۲ تعریف رسمی حالت‌ها

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 فقط‌خواندنی بدون تغییر مانده منطقی

۲.۴ آیا دادهٔ قبلی «خراب» می‌شود؟

اگر درست پیاده شود: خیر.

شرایط ایمنی:

  1. ستون‌های جدید nullable باشند و migration فقط ADD COLUMN باشد (بدون NOT NULL فوری).
  2. همه خواننده‌ها این اولویت را رعایت کنند:
اگر 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 فقط با لاگ هشدار)
  1. نوشته‌های جدید (پس از فاز مربوطه) همیشه base را persist کنند.
  2. هیچ 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 پوشش داده شود)

  1. تغییر نرخ بعد از ثبت سند → بدون snapshot، گزارش تاریخی عوض می‌شود. به همین دلیل persist پایه مهم است.
  2. fallback نرخ ۱ در کد فعلی مانده شخص/بستن سال → پس از فعال‌سازی خط، باید برای دادهٔ جدید ممنوع و برای قدیم قابل‌ردیابی باشد.
  3. سال بسته‌شده → backfill نباید سند افتتاحیه سال بعد را جابه‌جا کند.
  4. اسناد ترکیبی آینده (پرداخت بین‌ارزی) → مدل «یک ارز برای کل سند» ممکن است کافی نباشد؛ فاز ۳ باید تصمیم 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

  1. در پاسخ جزئیات فاکتور فیلدهای محاسبه‌شده پایدار:
    • totals.foreign
    • totals.base
    • fx (از snapshot؛ اگر نبود از resolve — ترجیح: snapshot اجباری بماند)
  2. اطمینان از پر شدن extra_info.fx در create/update همه زیرنوع‌های invoice موردنیاز
  3. تست: تک‌ارزی → بدون الزام فیلدهای dual؛ چندارزی ارزی → fx و totals.base موجود

Frontend

  1. بلوک جمع دوگانه در فرم فاکتور (پشت Gate)
  2. چاپ و لینک عمومی همان منطق
  3. عدم نمایش وقتی ارز = پایه یا 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

  1. Migration لایه ۱ (nullable only)
  2. Helper واحد: line_amounts_to_base(document, line) -> (debit_base, credit_base, rate, source)
  3. در create/update اسناد (حداقل invoice؛ سپس receipts/transfers/expenses):
    • اگر ارز سند = پایه → می‌توان NULL گذاشت یا rate=1 پر کرد (ترجیح: برای سادگی گزارش، rate=1 و base=amount)
    • اگر ارزی → از extra_info.fx.rate پر کردن اجباری
  4. Job backfill لایه ۲ (idempotent، batch، قابل توقف)
  5. خواندن گزارش/مانده/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

  1. شل کردن _validate_invoice_payment_item_currency فقط وقتی:
    • MC=ON
    • و فیلدهای settles/fx معتبر
  2. مسیر هم‌ارز فعلی دست‌نخورده بماند
  3. تولید خطوط سند با base پرشده (وابسته به P2.5)
  4. ذخیره fx per payment item در extra_info
  5. همان الگو برای receipt_payment_service (اقساط/تهاتر)
  6. خطاهای راهنما به فارسی وقتی نرخ وارد نشده

Frontend

  1. در ویجت پرداخت فاکتور: اگر حساب ≠ ارز فاکتور و MC=ON → فیلد نرخ + مبلغ تسویه ارزی
  2. لیست پرداخت‌ها با نمایش دوگانه
  3. 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 — تست، مشاهده‌پذیری، سخت‌سازی (مستمر)

حداقل پوشش هر فاز:

  1. API تک‌ارزی (باید امن/مخفی)
  2. API چندارزی happy path
  3. رگرسیون پرداخت هم‌ارز
  4. گرد کردن اعشار
  5. golden test برای Gate در Flutter (در صورت امکان)
  6. لاگ نرخ و 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)

موج ۱ — ارزش سریع بدون ریسک داده

  1. P2 نمایش دوگانه فاکتور
  2. P5 مانده شخص per currency

موج ۲ — زیرساخت حسابداری

  1. P2.5 ستون پایه + helper واحد + backfill لایه ۲
  2. P7 یکدست‌سازی گزارش‌ها روی helper جدید

موج ۳ — عملیاتی

  1. P3 پرداخت/دریافت بین‌ارزی (پس از D1)
  2. P4 انتقال بین‌ارزی (پس از D4)

موج ۴ — تکمیل

  1. P6 قیمت کالا
  2. P8 تسعیر پایان دوره (اگر D8 تأیید شد)
  3. سخت‌سازی P9 و پاکسازی fallback نرخ ۱ در مسیرهای حیاتی

هر موج فقط وقتی merge می‌شود که چک‌لیست تک‌ارزی سبز باشد.


۹. معیارهای پذیرش سراسری (هر موج)

۹.۱ کسب‌وکار تک‌ارزی

  • هیچ منو/فیلد/فیلتر FX جدید
  • مانده‌ها و گزارش‌ها عدد قبلی را حفظ می‌کنند (تلورانس گرد کردن صفر برای ارز بدون اعشار)
  • APIهای جدید MC برای این کسب‌وکار امن‌اند

۹.۲ کسب‌وکار چندارزی

  • نرخ و ارز در تراکنش‌های جدید snapshot می‌شوند
  • گزارش «یک ارز» و «همه» معنای شفاف دارند
  • پرداخت هم‌ارز رگرسیون ندارد

۹.۳ داده تاریخی

  • اسناد قدیمی بدون crash خوانده می‌شوند
  • backfill قابل تکرار و قابل گزارش است
  • سال بسته‌شده بدون ابزار صریح تغییر نمی‌کند

۱۰. خارج از محدوده این برنامه اجرایی

  • چند ارز پایه همزمان برای یک کسب‌وکار
  • رمزارز / نرخ لحظه‌ای معاملات
  • تغییر قوانین سامانه مودیان برای فاکتور غیرریالی (مسیر قانونی جدا)
  • اجازه تغییر ارز پایه پس از شروع عملیات بدون پروژه مهاجرت مستقل

۱۱. نحوه پیشبرد کار روی این سند

  1. تصمیم‌های باز موردنیاز موج بعدی را قفل کنید.
  2. برای هر فاز issue/PR با عنوان MC-Exec-P#: ... و لینک به بخش همین فایل باز کنید.
  3. قبل از شروع P2.5، این سند را با D7 (لزوم line.currency_id) به‌روز کنید.
  4. پس از اتمام هر فاز، چک‌باکس‌ها و وضعیت جدول بخش ۴ را به‌روز کنید و commit/PR را ثبت کنید.
  5. تا وقتی چک‌لیست 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 پرداخت/انتقال فقط وقتی یکی پایه → v2 فرعی↔فرعی با E1/E2
L2 UI انتقال بین‌ارزی ناقص → فرم انتقال destination/fx در V2-P0
L3 تسعیر پایان دوره فقط بانک/صندوق/تنخواه → در v2 شخص و چک باز هم پوشش داده شد
L4 روزنامه «همه» جمع خام → V2-P1 از *_base
L5 فیلتر ارز گزارش پشت Gate → V2-P0 برای TB/روزنامه/PnL/BS
L6 دریافت/پرداخت مستقل مسیر بین‌ارزی → V2-P4
L7 چاپ/لینک عمومی جمع دوگانه → PDF و صفحه عمومی در V2-P5
L8 حذف آخرین ارز فرعی قفل نیست (D6) → V2-P7 usage گسترده

۱۷. برنامه اجرایی نسخه ۲ (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 کامل‌تر

۱۷.۱ هدف نسخه ۲

رساندن محصول از «چندارزی عملیاتی پایه (با قید ارز پایه)» به «چندارزی عملیاتی کامل‌تر برای کسب‌وکارهای چندارز واقعی»:

  1. بستن شکاف‌های UI/گزارش v1 که API دارد ولی کاربر نمی‌بیند
  2. تسویه و انتقال فرعی↔فرعی (مثلاً USD↔EUR) با مدل نرخ شفاف
  3. تسعیر پایان دوره برای مانده‌های پولی کامل‌تر (شخص، چک)
  4. یکدست‌سازی گزارش‌ها و دفاع در عمق Gate
  5. سخت‌سازی داده و قفل‌های ایمنی ارز

۱۷.۲ نقشه فازهای 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

  1. بکاپ DB قبل از موج اول v2
  2. قفل E1/E2 قبل از شروع V2-P3
  3. قفل E4/E5 قبل از V2-P2
  4. issue/PR با پیشوند MC-v2-P#
  5. پس از هر فاز، همین بخش ۱۷ را به‌روز کنید

۱۸. جمع‌بندی یک‌صفحه‌ای برای مدیریت (v2)

  • v1 چندارزی عملیاتی را با قید «یکی از ارزها باید پایه باشد» و تسعیر نقدی تحویل داد.
  • v2 باید اول UI/گزارش‌های نصفه را ببندد، بعد تسعیر اشخاص، بعد فرعی↔فرعی.
  • پرریسک‌ترین کار: V2-P3 (USD↔EUR) — بدون قفل E1/E2 شروع نشود.
  • تک‌ارزی همچنان پشت Gate می‌ماند.