15 KiB
Executable file
تحلیل انتقال کالا و خدمات از hesabixOld به hesabixpy
خلاصه اجرایی
این سند تحلیل نحوه انتقال کالا و خدمات (commodities/products) از دیتابیس قدیمی (hesabixOld) به دیتابیس جدید (hesabixpy) را ارائه میدهد.
وضعیت فعلی
دیتابیس قدیمی (hesabixOld)
جدول commodity
- تعداد کالا/خدمات: 213,565
- تعداد کسب و کارهای دارای کالا: 2,371
- ساختار:
id: شناسه یکتاbid_id: شناسه کسب و کار (ارجاع بهbusiness.id)name: نام کالا/خدمتcode: کد کالا/خدمتdes: توضیحاتprice_buy: قیمت خرید (varchar)price_sell: قیمت فروش (varchar)khadamat: نوع (0 = کالا، 1 = خدمت، NULL = کالا)cat_id: شناسه دستهبندی (ارجاع بهcommodity_cat.id)unit_id: شناسه واحد (ارجاع بهcommodity_unit.id)order_point: نقطه سفارشcommodity_count_check: بررسی موجودیmin_order_count: حداقل تعداد سفارشday_loading: روز بارگذاریspeed_access: دسترسی سریعwithout_tax: بدون مالیاتbarcodes: بارکدها (longtext)tax_code: کد مالیاتیtax_type: نوع مالیاتtax_unit: واحد مالیاتcustom_code: کد سفارشیtags: تگها (JSON)
توزیع نوع:
- کالا (khadamat = 0): 206,291 (کل) / 196,246 (قابل انتقال - 96.6%)
- خدمت (khadamat = 1): 7,269 (کل) / 6,867 (قابل انتقال - 3.4%)
- NULL: 5 (کل) / 4 (قابل انتقال)
دستهبندی (قابل انتقال):
- کالا/خدمات با دستهبندی: 68,710 (33.8%)
- کالا/خدمات بدون دستهبندی: 134,407 (66.2%)
واحد:
- همه کالا/خدمات واحد دارند (213,565)
جدول commodity_cat
- ساختار:
id: شناسه یکتاbid_id: شناسه کسب و کارname: نام دستهبندیupper: دستهبندی والدroot: آیا ریشه است؟
جدول commodity_unit
- ساختار:
id: شناسه یکتاname: نام واحد (مثل "عدد", "کیلوگرم", "لیتر")float_number: تعداد اعشار
دیتابیس جدید (hesabixpy)
جدول products
- تعداد کالا/خدمات فعلی: 3,453
- ساختار:
id: شناسه یکتاbusiness_id: شناسه کسب و کار (ارجاع بهbusinesses.id)item_type: نوع آیتم (ENUM: "کالا", "خدمت")code: کد یکتا در هر کسب و کارname: نامdescription: توضیحاتcategory_id: شناسه دستهبندی (ارجاع بهcategories.id)main_unit: واحد اصلی (varchar - نام واحد)secondary_unit: واحد فرعی (varchar)unit_conversion_factor: ضریب تبدیل واحدbase_sales_price: قیمت فروش پایه (decimal)base_purchase_price: قیمت خرید پایه (decimal)track_inventory: ردیابی موجودی (boolean)reorder_point: نقطه سفارش مجدد (integer)min_order_qty: حداقل تعداد سفارش (integer)lead_time_days: زمان تحویل (integer)is_sales_taxable: مالیاتپذیر فروش (boolean)is_purchase_taxable: مالیاتپذیر خرید (boolean)sales_tax_rate: نرخ مالیات فروش (decimal)purchase_tax_rate: نرخ مالیات خرید (decimal)tax_type_id: شناسه نوع مالیاتtax_code: کد مالیاتیtax_unit_id: شناسه واحد مالیاتinventory_mode: حالت موجودی (varchar - پیشفرض: "bulk")track_serial: ردیابی سریال (boolean)track_barcode: ردیابی بارکد (boolean)created_at: تاریخ ایجادupdated_at: تاریخ بهروزرسانی
جدول categories
- ساختار:
id: شناسه یکتاbusiness_id: شناسه کسب و کارparent_id: شناسه دستهبندی والدtitle_translations: عناوین چندزبانه (JSON - {"fa": "...", "en": "..."})description: توضیحاتsort_order: ترتیب نمایشis_active: فعال بودنcreated_at: تاریخ ایجادupdated_at: تاریخ بهروزرسانی
استراتژی انتقال
تبدیل فیلدها
| فیلد قدیمی | فیلد جدید | تبدیل |
|---|---|---|
id |
- | نگهداری در mapping table (اختیاری) |
bid_id |
business_id |
نگاشت از business_id_mapping |
name |
name |
مستقیم |
code |
code |
مستقیم |
des |
description |
مستقیم |
price_buy |
base_purchase_price |
تبدیل varchar به decimal |
price_sell |
base_sales_price |
تبدیل varchar به decimal |
khadamat |
item_type |
0 یا NULL → "کالا", 1 → "خدمت" |
cat_id |
category_id |
نگاشت از category_id_mapping |
unit_id |
main_unit |
نگاشت از unit_id به unit name |
order_point |
reorder_point |
تبدیل varchar به integer |
commodity_count_check |
track_inventory |
تبدیل boolean |
min_order_count |
min_order_qty |
تبدیل varchar به integer |
day_loading |
lead_time_days |
تبدیل varchar به integer |
without_tax |
is_sales_taxable, is_purchase_taxable |
معکوس boolean |
barcodes |
- | حذف (یا در جدول جداگانه) |
tax_code |
tax_code |
مستقیم |
tax_type |
tax_type_id |
نیاز به mapping |
tax_unit |
tax_unit_id |
نیاز به mapping |
tags |
- | حذف (یا در جدول جداگانه) |
تبدیل khadamat به item_type
الگوریتم:
def convert_khadamat_to_item_type(khadamat: int | None) -> str:
"""
تبدیل khadamat به item_type
Args:
khadamat: مقدار khadamat از دیتابیس قدیمی (0 = کالا، 1 = خدمت، NULL = کالا)
Returns:
"کالا" یا "خدمت"
"""
if khadamat == 1:
return "خدمت"
return "کالا" # پیشفرض برای 0 و NULL
تبدیل قیمتها
الگوریتم:
def convert_price(price_str: str | None) -> Decimal | None:
"""
تبدیل قیمت از varchar به decimal
Args:
price_str: قیمت به صورت string (مثل "5250000" یا "0")
Returns:
Decimal یا None
"""
if not price_str or not price_str.strip():
return None
try:
# حذف فاصله و کاراکترهای غیرعددی (به جز نقطه و منفی)
cleaned = price_str.strip().replace(',', '').replace(' ', '')
if not cleaned or cleaned == '0':
return None
return Decimal(cleaned)
except (ValueError, InvalidOperation):
return None
نگاشت category_id
چالش:
- در قدیمی:
commodity_catباbid_idوname - در جدید:
categoriesباbusiness_idوtitle_translations(JSON)
راهحل:
- برای هر کسب و کار، دستهبندیهای قدیمی را بخوان
- در دیتابیس جدید جستجو کن (بر اساس business_id و name)
- اگر پیدا نشد: ایجاد کن با
title_translations = {"fa": name, "en": name} - mapping را ذخیره کن
الگوریتم:
def get_or_create_category(
old_cat_id: int | None,
old_cat_name: str | None,
new_business_id: int,
category_mapping: Dict[int, int]
) -> int | None:
"""
دریافت یا ایجاد دستهبندی
Returns:
شناسه دستهبندی جدید یا None
"""
if not old_cat_id or not old_cat_name:
return None
# بررسی mapping
if old_cat_id in category_mapping:
return category_mapping[old_cat_id]
# جستجو در دیتابیس جدید
query = text("""
SELECT id FROM categories
WHERE business_id = :business_id
AND JSON_EXTRACT(title_translations, '$.fa') = :name
LIMIT 1
""")
result = db.execute(query, {
"business_id": new_business_id,
"name": old_cat_name
}).fetchone()
if result:
category_mapping[old_cat_id] = result[0]
return result[0]
# ایجاد دستهبندی جدید
query = text("""
INSERT INTO categories (
business_id, title_translations,
sort_order, is_active,
created_at, updated_at
) VALUES (
:business_id, :title_translations,
0, 1,
:created_at, :updated_at
)
""")
title_translations = json.dumps({"fa": old_cat_name, "en": old_cat_name})
db.execute(query, {
"business_id": new_business_id,
"title_translations": title_translations,
"created_at": datetime.utcnow(),
"updated_at": datetime.utcnow()
})
db.commit()
# دریافت شناسه جدید
new_cat_id = db.lastrowid
category_mapping[old_cat_id] = new_cat_id
return new_cat_id
نگاشت unit_id به main_unit
چالش:
- در قدیمی:
unit_id(ارجاع بهcommodity_unit.id) - در جدید:
main_unit(varchar - نام واحد)
راهحل:
- ایجاد mapping بین
unit_idوunit.name - استفاده از نام واحد به عنوان
main_unit
الگوریتم:
def create_unit_mapping() -> Dict[int, str]:
"""ایجاد mapping بین unit_id و unit name"""
query = text("SELECT id, name FROM hesabixOld.commodity_unit")
results = old_db.execute(query).fetchall()
mapping = {}
for row in results:
mapping[row.id] = row.name
return mapping
تبدیل سایر فیلدها
order_point → reorder_point:
def convert_order_point(order_point: str | None) -> int | None:
if not order_point or not order_point.strip():
return None
try:
return int(float(order_point.strip()))
except (ValueError, TypeError):
return None
commodity_count_check → track_inventory:
def convert_track_inventory(check: int | None) -> bool:
return bool(check) if check is not None else False
without_tax → is_sales_taxable/is_purchase_taxable:
def convert_taxable(without_tax: int | None) -> tuple[bool, bool]:
"""
تبدیل without_tax به is_sales_taxable و is_purchase_taxable
Returns:
(is_sales_taxable, is_purchase_taxable)
"""
if without_tax == 1:
return (False, False) # بدون مالیات
return (True, True) # با مالیات (پیشفرض)
الگوریتم انتقال
مرحله 1: آمادهسازی
- ایجاد business_id_mapping: از انتقال کسب و کارها
- ایجاد unit_mapping: mapping بین unit_id و unit name
- ایجاد category_mapping: برای هر کسب و کار
مرحله 2: فیلتر کالا/خدمات برای انتقال
شرایط انتقال:
- کالا/خدمت باید
bid_idداشته باشد bid_idباید درbusiness_id_mappingوجود داشته باشد- کالا/خدمت نباید در دیتابیس جدید وجود داشته باشد (بر اساس business_id و code)
SQL برای انتخاب:
SELECT c.*
FROM hesabixOld.commodity c
INNER JOIN business_id_mapping m ON c.bid_id = m.old_business_id
WHERE NOT EXISTS (
SELECT 1 FROM hesabixpy.products new
WHERE new.business_id = m.new_business_id
AND new.code = c.code
)
ORDER BY c.bid_id, c.id;
مرحله 3: پردازش هر کالا/خدمت
مراحل:
- نگاشت
bid_id→business_id - تبدیل
khadamat→item_type - تبدیل
price_buyوprice_sell→base_purchase_priceوbase_sales_price - نگاشت
cat_id→category_id(با ایجاد در صورت نیاز) - نگاشت
unit_id→main_unit(نام واحد) - تبدیل سایر فیلدها
- درج در دیتابیس جدید
مرحله 4: مدیریت موارد خاص
سناریو 1: category_id نامعتبر
- عمل:
category_id = NULL - لاگ: ثبت در لاگ
سناریو 2: unit_id نامعتبر
- عمل:
main_unit = NULLیا پیشفرض "عدد" - لاگ: ثبت در لاگ
سناریو 3: قیمتهای نامعتبر
- عمل:
base_sales_price = NULLیاbase_purchase_price = NULL - لاگ: ثبت در لاگ
سناریو 4: کد تکراری
- عمل: skip کردن با reason "duplicate_code"
- لاگ: ثبت در لاگ
آمار و ارقام
توزیع دادهها
- کل کالا/خدمات: 213,565
- کالا (khadamat = 0): 206,291 (96.6%)
- خدمت (khadamat = 1): 7,269 (3.4%)
- کالا/خدمات با دستهبندی: 77,160 (36.1%)
- کالا/خدمات بدون دستهبندی: 136,405 (63.9%)
- کسب و کارهای دارای کالا: 2,371
قابل انتقال
- کسب و کارهای منتقل شده: 3,812
- کسب و کارهای دارای کالا که منتقل شدهاند: 2,146
- کالا/خدمات قابل انتقال: 203,117
- کالا/خدمات غیرقابل انتقال: 10,448 (کسب و کار owner منتقل نشده)
نکات مهم
- حجم زیاد داده: 213,565 کالا/خدمات - نیاز به پردازش batch به batch
- دستهبندیها: باید برای هر کسب و کار ایجاد شوند
- واحدها: باید از ID به نام تبدیل شوند
- قیمتها: باید از varchar به decimal تبدیل شوند
- کد یکتا: کد باید در هر کسب و کار یکتا باشد
ریسکها و راهحلها
ریسک 1: حجم زیاد داده
راهحل: پردازش batch به batch با اندازه مناسب (100-500 در هر batch)
ریسک 2: دستهبندیهای تکراری
راهحل: بررسی وجود قبل از ایجاد
ریسک 3: کدهای تکراری
راهحل: بررسی unique constraint قبل از درج
ریسک 4: قیمتهای نامعتبر
راهحل: تبدیل با مدیریت خطا و استفاده از NULL
ریسک 5: واحدهای نامعتبر
راهحل: استفاده از پیشفرض "عدد" در صورت عدم وجود
نتیجهگیری
انتقال کالا و خدمات نیاز به:
- ✅ نگاشت business_id
- ✅ تبدیل item_type
- ✅ تبدیل قیمتها
- ✅ ایجاد/نگاشت دستهبندیها
- ✅ تبدیل واحدها
- ✅ پردازش batch به batch به دلیل حجم زیاد
تخمین زمان: با batch size 500، حدود 406 batch نیاز است که میتواند 30-60 دقیقه طول بکشد.
آمار دقیق قابل انتقال:
- کالا: 196,246
- خدمت: 6,867
- با دستهبندی: 68,710
- بدون دستهبندی: 134,407
- کسب و کارهای دارای کالا: 2,146