arc/docs/FISCAL_YEAR_MIGRATION_ANALYSIS.md
2026-04-14 19:34:55 +03:30

14 KiB
Executable file

تحلیل انتقال سال‌های مالی از hesabixOld به hesabixpy

خلاصه اجرایی

این سند تحلیل نحوه انتقال سال‌های مالی (fiscal years) از دیتابیس قدیمی (hesabixOld) به دیتابیس جدید (hesabixpy) را ارائه می‌دهد.

وضعیت فعلی

دیتابیس قدیمی (hesabixOld)

جدول year

  • تعداد سال‌های مالی: 4,473 سال مالی
  • ساختار:
    • id: شناسه یکتا
    • bid_id: شناسه کسب و کار (ارجاع به business.id)
    • label: عنوان سال مالی (مثل "سال مالی منتهی به 1403/2/27")
    • head: آیا سال مالی فعال است؟ (tinyint - 1 = فعال، 0 = غیرفعال)
    • start: تاریخ شروع (varchar timestamp - مثل "1684322286")
    • end: تاریخ پایان (varchar timestamp - مثل "1715858286")
    • now: تاریخ فعلی (varchar timestamp - معمولاً NULL)

ویژگی‌ها:

  • هر کسب و کار فقط یک سال مالی دارد
  • همه سال‌های مالی head = 1 دارند (همه فعال هستند)
  • start و end به صورت timestamp (Unix timestamp) ذخیره شده‌اند
  • now همیشه NULL است
  • همه تاریخ‌ها معتبر هستند (هیچ NULL یا خالی نیست)

نمونه داده:

id | bid_id | label                        | head | start      | end        | now
2  | 4      | سال مالی منتهی به 1403/2/27 | 1    | 1684322286 | 1715858286 | NULL

دیتابیس جدید (hesabixpy)

جدول fiscal_years

  • تعداد سال‌های مالی فعلی: 64 سال مالی
  • ساختار:
    • id: شناسه یکتا
    • business_id: شناسه کسب و کار (ارجاع به businesses.id - ForeignKey)
    • title: عنوان سال مالی
    • start_date: تاریخ شروع (DATE)
    • end_date: تاریخ پایان (DATE)
    • is_last: آیا آخرین سال مالی فعال است؟ (boolean)
    • inventory_valuation_method: روش ارزیابی انبار (varchar - پیش‌فرض: "FIFO")
    • created_at: تاریخ ایجاد
    • updated_at: تاریخ به‌روزرسانی

ویژگی‌ها:

  • یک کسب و کار می‌تواند چندین سال مالی داشته باشد
  • فقط یک سال مالی می‌تواند is_last = true باشد
  • تاریخ‌ها به صورت DATE ذخیره می‌شوند (نه timestamp)
  • فیلد inventory_valuation_method جدید است (پیش‌فرض: "FIFO")

استراتژی انتقال

تبدیل فیلدها

فیلد قدیمی فیلد جدید تبدیل
id - نگه‌داری در mapping table (اختیاری)
bid_id business_id نگاشت از business_id_mapping
label title مستقیم
head is_last تبدیل boolean (1 → true, 0 → false)
start start_date تبدیل timestamp به DATE
end end_date تبدیل timestamp به DATE
now - حذف (همیشه NULL است)
- inventory_valuation_method پیش‌فرض: "FIFO"
- created_at datetime.utcnow()
- updated_at datetime.utcnow()

تبدیل timestamp به DATE

الگوریتم:

def convert_timestamp_to_date(timestamp_str: str | None) -> date | None:
    """
    تبدیل timestamp string به date
    
    Args:
        timestamp_str: timestamp به صورت string (مثل "1684322286")
    
    Returns:
        date object یا None
    """
    if not timestamp_str or not timestamp_str.strip():
        return None
    
    try:
        # تبدیل string به int
        timestamp = int(timestamp_str.strip())
        # تبدیل timestamp به datetime
        dt = datetime.fromtimestamp(timestamp)
        # تبدیل به date
        return dt.date()
    except (ValueError, TypeError, OSError):
        return None

مثال:

  • "1684322286" → date(2023, 5, 17)
  • "1715858286" → date(2024, 5, 16)

تبدیل head به is_last

الگوریتم:

def convert_head_to_is_last(head: int | None) -> bool:
    """
    تبدیل head به is_last
    
    Args:
        head: مقدار head از دیتابیس قدیمی (1 = فعال، 0 = غیرفعال)
    
    Returns:
        boolean (true = آخرین سال مالی فعال)
    """
    return bool(head) if head is not None else False

نکته: در دیتابیس قدیمی همه سال‌های مالی head = 1 دارند، پس همه is_last = true خواهند بود.

نگاشت business_id

الگوریتم:

  1. استفاده از business_id_mapping که در انتقال کسب و کارها ایجاد شده
  2. جستجو برای old_business_id = bid_id
  3. اگر پیدا شد: استفاده از new_business_id
  4. اگر پیدا نشد: skip کردن سال مالی (کسب و کار منتقل نشده)

SQL برای ایجاد mapping:

-- استفاده از business_id_mapping از انتقال کسب و کارها
-- یا ایجاد مجدد:
SELECT 
    old_business.id as old_business_id,
    new_business.id as new_business_id
FROM hesabixOld.business old_business
INNER JOIN hesabixOld.user old_user ON old_business.owner_id = old_user.id
INNER JOIN hesabixpy.users new_user ON (
    (old_user.email IS NOT NULL AND new_user.email IS NOT NULL AND old_user.email = new_user.email) OR
    (old_user.mobile IS NOT NULL AND new_user.mobile IS NOT NULL AND old_user.mobile = new_user.mobile)
)
INNER JOIN hesabixpy.businesses new_business ON (
    new_business.owner_id = new_user.id 
    AND new_business.name = old_business.name
);

الگوریتم انتقال

مرحله 1: فیلتر سال‌های مالی برای انتقال

شرایط انتقال:

  1. سال مالی باید bid_id داشته باشد
  2. bid_id باید در business_id_mapping وجود داشته باشد (کسب و کار منتقل شده باشد)
  3. start و end باید معتبر باشند (نه NULL و نه خالی)
  4. سال مالی نباید در دیتابیس جدید وجود داشته باشد (بر اساس business_id و title)

SQL برای انتخاب سال‌های مالی:

SELECT y.*
FROM hesabixOld.year y
INNER JOIN business_id_mapping m ON y.bid_id = m.old_business_id
WHERE y.start IS NOT NULL 
  AND y.end IS NOT NULL
  AND y.start != ''
  AND y.end != ''
  AND NOT EXISTS (
    SELECT 1 FROM hesabixpy.fiscal_years new
    WHERE new.business_id = m.new_business_id
      AND new.title = y.label
  )
ORDER BY y.id;

مرحله 2: پردازش هر سال مالی

مراحل پردازش:

  1. بررسی business_id:

    • جستجو در business_id_mapping
    • اگر پیدا نشد: skip با reason "business_not_migrated"
  2. تبدیل داده‌ها:

    • تبدیل start (timestamp) به start_date (DATE)
    • تبدیل end (timestamp) به end_date (DATE)
    • تبدیل head به is_last (boolean)
    • استفاده از label به عنوان title
  3. ایجاد سال مالی جدید:

    • درج در جدول fiscal_years
    • تنظیم business_id به new_business_id
    • تنظیم inventory_valuation_method به "FIFO" (پیش‌فرض)
    • تنظیم created_at و updated_at
  4. مدیریت خطاها:

    • در صورت خطا: ثبت در لاگ با جزئیات
    • ادامه با سال مالی بعدی

مرحله 3: مدیریت موارد خاص

سناریو 1: business_id در دیتابیس جدید وجود ندارد

  • بررسی: آیا bid_id در business_id_mapping وجود دارد؟
  • عمل: skip کردن با reason "business_not_migrated"
  • ثبت در لاگ

سناریو 2: تاریخ‌های نامعتبر

  • بررسی: آیا start و end قابل تبدیل به date هستند؟
  • عمل: skip کردن با reason "invalid_dates"
  • ثبت در لاگ

سناریو 3: سال مالی تکراری

  • بررسی: آیا سال مالی با همان business_id و title در دیتابیس جدید وجود دارد؟
  • عمل: skip کردن با reason "already_exists"
  • ثبت در لاگ

آمار و ارقام

توزیع داده‌ها

  • کل سال‌های مالی: 4,473
  • سال‌های مالی با head = 1: 4,473 (100%)
  • سال‌های مالی با تاریخ معتبر: 4,473 (100%)
  • سال‌های مالی با now = NULL: 4,473 (100%)
  • حداکثر سال مالی برای هر کسب و کار: 1

Mapping

  • کسب و کارهای قابل انتقال: ~4,062 (با owner که در دیتابیس جدید وجود دارد)
  • سال‌های مالی قابل انتقال: 4,062 (با کسب و کار که owner در دیتابیس جدید دارد)
  • سال‌های مالی غیرقابل انتقال: 411 (کسب و کار owner در دیتابیس جدید ندارد)

کد نمونه

def convert_timestamp_to_date(timestamp_str: str | None) -> date | None:
    """تبدیل timestamp string به date"""
    if not timestamp_str or not timestamp_str.strip():
        return None
    
    try:
        timestamp = int(timestamp_str.strip())
        dt = datetime.fromtimestamp(timestamp)
        return dt.date()
    except (ValueError, TypeError, OSError):
        return None

def migrate_fiscal_year(
    old_year: Dict[str, Any],
    business_id_mapping: Dict[int, int]
) -> Optional[int]:
    """انتقال یک سال مالی"""
    try:
        # نگاشت business_id
        old_business_id = old_year.get('bid_id')
        new_business_id = business_id_mapping.get(old_business_id)
        
        if not new_business_id:
            return None  # کسب و کار منتقل نشده
        
        # تبدیل تاریخ‌ها
        start_date = convert_timestamp_to_date(old_year.get('start'))
        end_date = convert_timestamp_to_date(old_year.get('end'))
        
        if not start_date or not end_date:
            return None  # تاریخ‌های نامعتبر
        
        # تبدیل head به is_last
        is_last = bool(old_year.get('head', 0))
        
        # درج در دیتابیس جدید
        query = text("""
            INSERT INTO fiscal_years (
                business_id, title, start_date, end_date,
                is_last, inventory_valuation_method,
                created_at, updated_at
            ) VALUES (
                :business_id, :title, :start_date, :end_date,
                :is_last, :inventory_valuation_method,
                :created_at, :updated_at
            )
        """)
        
        db.execute(query, {
            "business_id": new_business_id,
            "title": old_year.get('label'),
            "start_date": start_date,
            "end_date": end_date,
            "is_last": is_last,
            "inventory_valuation_method": "FIFO",
            "created_at": datetime.utcnow(),
            "updated_at": datetime.utcnow()
        })
        
        db.commit()
        return new_business_id
        
    except Exception as e:
        db.rollback()
        raise e

نکات مهم

  1. هر کسب و کار یک سال مالی: در دیتابیس قدیمی هر کسب و کار فقط یک سال مالی دارد
  2. همه سال‌های مالی فعال: همه head = 1 دارند، پس همه is_last = true خواهند بود
  3. تبدیل timestamp: باید از timestamp string به DATE تبدیل شود
  4. فیلد now: همیشه NULL است و نیازی به انتقال ندارد
  5. inventory_valuation_method: پیش‌فرض "FIFO" برای همه

ریسک‌ها و راه‌حل‌ها

ریسک 1: business_id نامعتبر

احتمال: متوسط (بسته به تعداد کسب و کارهای منتقل شده) راه‌حل: بررسی وجود در business_id_mapping قبل از انتقال

ریسک 2: تاریخ‌های نامعتبر

احتمال: بسیار کم (همه تاریخ‌ها معتبر هستند) راه‌حل: بررسی و skip کردن در صورت نامعتبر بودن

ریسک 3: تبدیل timestamp

احتمال: کم راه‌حل: استفاده از try-except و مدیریت خطا

ریسک 4: سال مالی تکراری

احتمال: کم (اگر کسب و کار قبلاً منتقل شده باشد) راه‌حل: بررسی وجود قبل از درج

مراحل اجرا

مرحله 1: ایجاد Business ID Mapping

# استفاده از mapping از انتقال کسب و کارها
business_id_mapping = get_business_id_mapping()

مرحله 2: خواندن سال‌های مالی

old_years = get_old_fiscal_years(business_id_mapping)

مرحله 3: انتقال

for old_year in old_years:
    migrate_fiscal_year(old_year, business_id_mapping)

مرحله 4: اعتبارسنجی

-- بررسی تعداد سال‌های مالی
SELECT COUNT(*) FROM hesabixpy.fiscal_years;

-- بررسی سال‌های مالی با is_last = true
SELECT COUNT(*) FROM hesabixpy.fiscal_years WHERE is_last = 1;

-- بررسی کسب و کارها با سال مالی
SELECT business_id, COUNT(*) as year_count
FROM hesabixpy.fiscal_years
GROUP BY business_id
HAVING year_count > 1;

نتیجه‌گیری

خلاصه:

  • ✅ همه سال‌های مالی قابل انتقال هستند (همه تاریخ‌ها معتبر)
  • ✅ تبدیل timestamp به DATE ساده است
  • ✅ همه سال‌های مالی is_last = true خواهند بود
  • ✅ نیاز به business_id_mapping از انتقال کسب و کارها

انتظار:

  • ~4,062 سال مالی منتقل می‌شوند (متناسب با کسب و کارها)
  • همه با is_last = true
  • همه با inventory_valuation_method = "FIFO"

وابستگی:

  • باید ابتدا کسب و کارها منتقل شوند
  • نیاز به business_id_mapping برای نگاشت bid_id → business_id