arc/hesabixAPI/scripts/migrate_businesses_complete.py
2025-12-09 02:51:12 +00:00

709 lines
22 KiB
Python
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
اسکریپت انتقال کامل کسب و کارها، سال‌های مالی و واحدهای ارزی از hesabixOld به hesabixpy
این اسکریپت:
- کسب و کارها را از hesabixOld منتقل می‌کند
- سال‌های مالی را منتقل می‌کند
- واحدهای ارزی (business_currencies) را منتقل می‌کند
"""
import sys
import os
import argparse
from typing import List, Dict, Any, Optional, Tuple
from datetime import datetime, date
import secrets
# اضافه کردن مسیر پروژه به sys.path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker, Session
import phonenumbers
def normalize_email(email: str | None) -> str | None:
"""نرمال‌سازی ایمیل"""
return email.lower().strip() if email else None
def normalize_mobile(mobile: str | None) -> str | None:
"""نرمال‌سازی موبایل"""
if not mobile:
return None
raw = mobile.strip()
raw = ''.join(ch for ch in raw if ch.isdigit() or ch == '+')
try:
region = None if raw.startswith('+') else "IR"
num = phonenumbers.parse(raw, region)
if not phonenumbers.is_valid_number(num):
return None
return phonenumbers.format_number(num, phonenumbers.PhoneNumberFormat.E164)
except Exception:
return None
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 convert_business_type(old_type: str | None) -> str:
"""تبدیل type به business_type (ENUM) - enum در دیتابیس به انگلیسی است"""
mapping = {
"فروشگاه": "STORE",
"مغازه": "SHOP",
"شخصی": "INDIVIDUAL",
"شرکت": "COMPANY",
"موسسه": "INSTITUTE",
"باشگاه": "CLUB",
"اتحادیه": "UNION"
}
if old_type and old_type in mapping:
return mapping[old_type]
return "SHOP" # پیش‌فرض
def convert_business_field(old_field: str | None) -> str:
"""تبدیل field به business_field (ENUM) - enum در دیتابیس به انگلیسی است"""
if not old_field:
return "OTHER"
old_field_lower = old_field.lower().strip()
# تولیدی
if any(keyword in old_field_lower for keyword in ["تولید", "ساخت"]):
return "MANUFACTURING"
# بازرگانی
if any(keyword in old_field_lower for keyword in ["بازرگانی", "فروش", "خرید", "تجارت"]):
return "TRADING"
# خدماتی
if any(keyword in old_field_lower for keyword in ["خدمات", "خدماتی", "مشاوره", "آموزش"]):
return "SERVICE"
# سایر
return "OTHER"
def split_full_name(full_name: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
"""تبدیل full_name به first_name و last_name"""
if not full_name or not full_name.strip():
return None, None
parts = full_name.strip().split()
if len(parts) == 0:
return None, None
elif len(parts) == 1:
return parts[0], None
else:
return parts[0], " ".join(parts[1:])
class BusinessMigration:
def __init__(self, old_db_name: str = "hesabixOld", new_db_name: str = "hesabixpy",
db_user: str = "root", db_password: str = "136431",
db_host: str = "localhost", db_port: int = 3306):
"""ایجاد اتصال به هر دو دیتابیس"""
self.old_db_name = old_db_name
self.new_db_name = new_db_name
# اتصال به دیتابیس قدیمی
old_dsn = f"mysql+pymysql://{db_user}:{db_password}@{db_host}:{db_port}/{old_db_name}"
old_engine = create_engine(old_dsn, echo=False, pool_pre_ping=True)
self.old_db = sessionmaker(bind=old_engine)()
# اتصال به دیتابیس جدید
new_dsn = f"mysql+pymysql://{db_user}:{db_password}@{db_host}:{db_port}/{new_db_name}"
new_engine = create_engine(new_dsn, echo=False, pool_pre_ping=True)
self.new_db = sessionmaker(bind=new_engine)()
# آمار
self.stats = {
"businesses_processed": 0,
"businesses_migrated": 0,
"businesses_skipped": 0,
"fiscal_years_migrated": 0,
"fiscal_years_skipped": 0,
"currencies_migrated": 0,
"currencies_skipped": 0,
"errors": 0,
"error_details": []
}
def create_user_id_mapping(self) -> Dict[int, int]:
"""ایجاد mapping بین user_id قدیمی و جدید"""
query = text("""
SELECT
old.id as old_user_id,
new.id as new_user_id
FROM hesabixOld.user old
INNER JOIN hesabixpy.users new ON (
(old.email IS NOT NULL AND new.email IS NOT NULL AND BINARY old.email = BINARY new.email) OR
(old.mobile IS NOT NULL AND new.mobile IS NOT NULL AND BINARY old.mobile = BINARY new.mobile)
)
WHERE old.active = 1
""")
results = self.old_db.execute(query).fetchall()
mapping = {}
for row in results:
mapping[row.old_user_id] = row.new_user_id
print(f"✅ User ID mapping ایجاد شد: {len(mapping)} کاربر")
return mapping
def create_currency_mapping(self) -> Dict[int, int]:
"""ایجاد mapping بین money_id قدیمی و currency_id جدید"""
query = text("""
SELECT
old_money.id as old_money_id,
new_currency.id as new_currency_id
FROM hesabixOld.money old_money
INNER JOIN hesabixpy.currencies new_currency
ON BINARY old_money.name = BINARY new_currency.code
""")
results = self.old_db.execute(query).fetchall()
mapping = {}
for row in results:
mapping[row.old_money_id] = row.new_currency_id
print(f"✅ Currency mapping ایجاد شد: {len(mapping)} ارز")
return mapping
def get_irr_currency_id(self) -> int:
"""دریافت شناسه ارز IRR"""
query = text("SELECT id FROM currencies WHERE code = 'IRR' LIMIT 1")
result = self.new_db.execute(query).fetchone()
if result:
return result[0]
return 1 # پیش‌فرض
def migrate_business(self, old_business: Dict[str, Any], user_id_mapping: Dict[int, int],
currency_mapping: Dict[int, int], irr_currency_id: int) -> Optional[int]:
"""انتقال یک کسب و کار"""
try:
# نگاشت owner_id
old_owner_id = old_business.get('owner_id')
new_owner_id = user_id_mapping.get(old_owner_id)
if not new_owner_id:
self.stats["businesses_skipped"] += 1
return None
# بررسی وجود در دیتابیس جدید
query = text("""
SELECT COUNT(*) FROM businesses
WHERE owner_id = :owner_id AND name = :name
""")
result = self.new_db.execute(query, {
"owner_id": new_owner_id,
"name": old_business.get('name')
}).scalar()
if result > 0:
# کسب و کار موجود است، شناسه آن را برگردان
query = text("""
SELECT id FROM businesses
WHERE owner_id = :owner_id AND name = :name
LIMIT 1
""")
result = self.new_db.execute(query, {
"owner_id": new_owner_id,
"name": old_business.get('name')
}).fetchone()
if result:
return result[0]
return None
# تبدیل داده‌ها
business_type = convert_business_type(old_business.get('type'))
business_field = convert_business_field(old_business.get('field'))
# تبدیل full_name (اگر legal_name وجود دارد)
first_name, last_name = split_full_name(old_business.get('legal_name'))
# نگاشت default_currency_id
old_money_id = old_business.get('money_id')
default_currency_id = currency_mapping.get(old_money_id, irr_currency_id) if old_money_id else irr_currency_id
# تبدیل تاریخ
date_submit = old_business.get('date_submit')
if date_submit and date_submit.strip():
try:
if date_submit.isdigit():
created_at = datetime.fromtimestamp(int(date_submit))
else:
created_at = datetime.utcnow()
except:
created_at = datetime.utcnow()
else:
created_at = datetime.utcnow()
# درج کسب و کار
query = text("""
INSERT INTO businesses (
name, business_type, business_field, owner_id,
national_id, registration_number, economic_id,
country, province, city, postal_code,
phone, mobile, address,
default_currency_id,
created_at, updated_at
) VALUES (
:name, :business_type, :business_field, :owner_id,
:national_id, :registration_number, :economic_id,
:country, :province, :city, :postal_code,
:phone, :mobile, :address,
:default_currency_id,
:created_at, :updated_at
)
""")
self.new_db.execute(query, {
"name": old_business.get('name'),
"business_type": business_type,
"business_field": business_field,
"owner_id": new_owner_id,
"national_id": old_business.get('shenasemeli'),
"registration_number": old_business.get('shomaresabt'),
"economic_id": old_business.get('codeeghtesadi'),
"country": old_business.get('country'),
"province": old_business.get('ostan'),
"city": old_business.get('shahrestan'),
"postal_code": old_business.get('postalcode'),
"phone": old_business.get('tel'),
"mobile": normalize_mobile(old_business.get('mobile')),
"address": old_business.get('address'),
"default_currency_id": default_currency_id,
"created_at": created_at,
"updated_at": datetime.utcnow()
})
self.new_db.commit()
# دریافت new_business_id
query = text("""
SELECT id FROM businesses
WHERE owner_id = :owner_id AND name = :name
LIMIT 1
""")
result = self.new_db.execute(query, {
"owner_id": new_owner_id,
"name": old_business.get('name')
}).fetchone()
if result:
self.stats["businesses_migrated"] += 1
return result[0]
else:
raise Exception("Failed to get new business ID after insert")
except Exception as e:
self.new_db.rollback()
self.stats["errors"] += 1
self.stats["error_details"].append({
"type": "business",
"old_business_id": old_business.get('id'),
"error": str(e)
})
return None
def migrate_fiscal_year(self, old_year: Dict[str, Any], business_id_mapping: Dict[int, int]) -> bool:
"""انتقال یک سال مالی"""
try:
old_business_id = old_year.get('bid_id')
new_business_id = business_id_mapping.get(old_business_id)
if not new_business_id:
self.stats["fiscal_years_skipped"] += 1
return False
# بررسی وجود
query = text("""
SELECT COUNT(*) FROM fiscal_years
WHERE business_id = :business_id AND title = :title
""")
result = self.new_db.execute(query, {
"business_id": new_business_id,
"title": old_year.get('label')
}).scalar()
if result > 0:
self.stats["fiscal_years_skipped"] += 1
return False
# تبدیل تاریخ‌ها
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:
self.stats["fiscal_years_skipped"] += 1
return False
# تبدیل 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
)
""")
self.new_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()
})
self.new_db.commit()
self.stats["fiscal_years_migrated"] += 1
return True
except Exception as e:
self.new_db.rollback()
self.stats["errors"] += 1
self.stats["error_details"].append({
"type": "fiscal_year",
"old_year_id": old_year.get('id'),
"error": str(e)
})
return False
def migrate_business_currency(self, old_bm: Dict[str, Any], business_id_mapping: Dict[int, int],
currency_mapping: Dict[int, int]) -> bool:
"""انتقال یک واحد ارزی کسب و کار"""
try:
old_business_id = old_bm.get('business_id')
new_business_id = business_id_mapping.get(old_business_id)
if not new_business_id:
self.stats["currencies_skipped"] += 1
return False
old_money_id = old_bm.get('money_id')
new_currency_id = currency_mapping.get(old_money_id)
if not new_currency_id:
self.stats["currencies_skipped"] += 1
return False
# بررسی وجود
query = text("""
SELECT COUNT(*) FROM business_currencies
WHERE business_id = :business_id AND currency_id = :currency_id
""")
result = self.new_db.execute(query, {
"business_id": new_business_id,
"currency_id": new_currency_id
}).scalar()
if result > 0:
self.stats["currencies_skipped"] += 1
return False
# درج
query = text("""
INSERT INTO business_currencies (
business_id, currency_id,
created_at, updated_at
) VALUES (
:business_id, :currency_id,
:created_at, :updated_at
)
""")
self.new_db.execute(query, {
"business_id": new_business_id,
"currency_id": new_currency_id,
"created_at": datetime.utcnow(),
"updated_at": datetime.utcnow()
})
self.new_db.commit()
self.stats["currencies_migrated"] += 1
return True
except Exception as e:
self.new_db.rollback()
self.stats["errors"] += 1
self.stats["error_details"].append({
"type": "business_currency",
"old_business_id": old_bm.get('business_id'),
"old_money_id": old_bm.get('money_id'),
"error": str(e)
})
return False
def get_old_businesses(self, start_id: Optional[int] = None, limit: Optional[int] = None) -> List[Dict[str, Any]]:
"""دریافت کسب و کارها از دیتابیس قدیمی"""
query = f"""
SELECT
id, owner_id, name, legal_name, money_id,
field, type, shenasemeli, codeeghtesadi, shomaresabt,
country, ostan, shahrestan, postalcode,
tel, mobile, address, date_submit
FROM {self.old_db_name}.business
WHERE owner_id IS NOT NULL
"""
params = {}
if start_id:
query += " AND id >= :start_id"
params["start_id"] = start_id
query += " ORDER BY id ASC"
if limit:
query += " LIMIT :limit"
params["limit"] = limit
results = self.old_db.execute(text(query), params).fetchall()
businesses = []
for row in results:
businesses.append({
"id": row[0],
"owner_id": row[1],
"name": row[2],
"legal_name": row[3],
"money_id": row[4],
"field": row[5],
"type": row[6],
"shenasemeli": row[7],
"codeeghtesadi": row[8],
"shomaresabt": row[9],
"country": row[10],
"ostan": row[11],
"shahrestan": row[12],
"postalcode": row[13],
"tel": row[14],
"mobile": row[15],
"address": row[16],
"date_submit": row[17]
})
return businesses
def get_old_fiscal_years(self, business_ids: List[int]) -> List[Dict[str, Any]]:
"""دریافت سال‌های مالی از دیتابیس قدیمی"""
if not business_ids:
return []
placeholders = ','.join([':id' + str(i) for i in range(len(business_ids))])
query = f"""
SELECT id, bid_id, label, head, start, end
FROM {self.old_db_name}.year
WHERE bid_id IN ({placeholders})
"""
params = {f'id{i}': bid for i, bid in enumerate(business_ids)}
results = self.old_db.execute(text(query), params).fetchall()
years = []
for row in results:
years.append({
"id": row[0],
"bid_id": row[1],
"label": row[2],
"head": row[3],
"start": row[4],
"end": row[5]
})
return years
def get_old_business_currencies(self, business_ids: List[int]) -> List[Dict[str, Any]]:
"""دریافت واحدهای ارزی از دیتابیس قدیمی"""
if not business_ids:
return []
placeholders = ','.join([':id' + str(i) for i in range(len(business_ids))])
query = f"""
SELECT business_id, money_id
FROM {self.old_db_name}.business_money
WHERE business_id IN ({placeholders})
"""
params = {f'id{i}': bid for i, bid in enumerate(business_ids)}
results = self.old_db.execute(text(query), params).fetchall()
currencies = []
for row in results:
currencies.append({
"business_id": row[0],
"money_id": row[1]
})
return currencies
def run_migration(self, dry_run: bool = False, batch_size: int = 100,
start_id: Optional[int] = None, limit: Optional[int] = None):
"""اجرای انتقال"""
print(f"{'='*60}")
print(f"شروع انتقال کسب و کارها، سال‌های مالی و واحدهای ارزی")
print(f"{'='*60}")
print(f"دیتابیس قدیمی: {self.old_db_name}")
print(f"دیتابیس جدید: {self.new_db_name}")
print(f"حالت تست (dry-run): {dry_run}")
print(f"اندازه batch: {batch_size}")
if start_id:
print(f"شروع از ID: {start_id}")
if limit:
print(f"محدودیت تعداد: {limit}")
print(f"{'='*60}\n")
# ایجاد mapping ها
user_id_mapping = self.create_user_id_mapping()
currency_mapping = self.create_currency_mapping()
irr_currency_id = self.get_irr_currency_id()
# دریافت کسب و کارها
old_businesses = self.get_old_businesses(start_id=start_id, limit=limit)
total_businesses = len(old_businesses)
print(f"تعداد کسب و کارها برای انتقال: {total_businesses}\n")
if total_businesses == 0:
print("هیچ کسب و کاری برای انتقال یافت نشد.")
return
# نگاشت old_business_id -> new_business_id
business_id_mapping: Dict[int, int] = {}
# پردازش batch به batch
for i in range(0, total_businesses, batch_size):
batch = old_businesses[i:i+batch_size]
batch_num = (i // batch_size) + 1
total_batches = (total_businesses + batch_size - 1) // batch_size
print(f"\nپردازش batch {batch_num}/{total_batches} ({len(batch)} کسب و کار)...")
for old_business in batch:
self.stats["businesses_processed"] += 1
if not dry_run:
new_business_id = self.migrate_business(
old_business, user_id_mapping, currency_mapping, irr_currency_id
)
if new_business_id:
business_id_mapping[old_business['id']] = new_business_id
else:
# در حالت dry-run فقط بررسی می‌کنیم
old_owner_id = old_business.get('owner_id')
if old_owner_id in user_id_mapping:
self.stats["businesses_migrated"] += 1
else:
self.stats["businesses_skipped"] += 1
if self.stats["businesses_processed"] % 10 == 0:
print(f" پردازش شده: {self.stats['businesses_processed']}/{total_businesses}", end='\r')
print(f"\n Batch {batch_num} تکمیل شد")
# انتقال سال‌های مالی و واحدهای ارزی
if not dry_run and business_id_mapping:
print(f"\n{'='*60}")
print("انتقال سال‌های مالی و واحدهای ارزی...")
print(f"{'='*60}\n")
old_business_ids = list(business_id_mapping.keys())
# انتقال سال‌های مالی
old_years = self.get_old_fiscal_years(old_business_ids)
print(f"سال‌های مالی برای انتقال: {len(old_years)}")
for old_year in old_years:
self.migrate_fiscal_year(old_year, business_id_mapping)
# انتقال واحدهای ارزی
old_currencies = self.get_old_business_currencies(old_business_ids)
print(f"واحدهای ارزی برای انتقال: {len(old_currencies)}")
for old_currency in old_currencies:
self.migrate_business_currency(old_currency, business_id_mapping, currency_mapping)
# نمایش آمار نهایی
print(f"\n{'='*60}")
print("آمار نهایی:")
print(f"{'='*60}")
print(f"کسب و کارها:")
print(f" پردازش شده: {self.stats['businesses_processed']}")
print(f" منتقل شده: {self.stats['businesses_migrated']}")
print(f" رد شده: {self.stats['businesses_skipped']}")
print(f"\nسال‌های مالی:")
print(f" منتقل شده: {self.stats['fiscal_years_migrated']}")
print(f" رد شده: {self.stats['fiscal_years_skipped']}")
print(f"\nواحدهای ارزی:")
print(f" منتقل شده: {self.stats['currencies_migrated']}")
print(f" رد شده: {self.stats['currencies_skipped']}")
print(f"\nخطاها: {self.stats['errors']}")
if self.stats['error_details']:
print(f"\nجزئیات خطاها:")
for error in self.stats['error_details'][:10]:
print(f" - {error.get('type')} (ID {error.get('old_business_id', error.get('old_year_id', 'N/A'))}): {error.get('error')}")
if len(self.stats['error_details']) > 10:
print(f" ... و {len(self.stats['error_details']) - 10} خطای دیگر")
def close(self):
"""بستن اتصالات"""
self.old_db.close()
self.new_db.close()
def main():
parser = argparse.ArgumentParser(description="انتقال کسب و کارها، سال‌های مالی و واحدهای ارزی")
parser.add_argument("--dry-run", action="store_true", help="اجرای تست بدون تغییر در دیتابیس")
parser.add_argument("--batch-size", type=int, default=100, help="تعداد کسب و کارها در هر batch")
parser.add_argument("--start-id", type=int, help="شروع از شناسه خاص")
parser.add_argument("--limit", type=int, help="محدود کردن تعداد کسب و کارها")
parser.add_argument("--old-db", default="hesabixOld", help="نام دیتابیس قدیمی")
parser.add_argument("--new-db", default="hesabixpy", help="نام دیتابیس جدید")
parser.add_argument("--db-user", default="root", help="نام کاربری دیتابیس")
parser.add_argument("--db-password", default="136431", help="رمز عبور دیتابیس")
parser.add_argument("--db-host", default="localhost", help="آدرس دیتابیس")
parser.add_argument("--db-port", type=int, default=3306, help="پورت دیتابیس")
args = parser.parse_args()
migration = BusinessMigration(
old_db_name=args.old_db,
new_db_name=args.new_db,
db_user=args.db_user,
db_password=args.db_password,
db_host=args.db_host,
db_port=args.db_port
)
try:
migration.run_migration(
dry_run=args.dry_run,
batch_size=args.batch_size,
start_id=args.start_id,
limit=args.limit
)
finally:
migration.close()
if __name__ == "__main__":
main()