Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/app/integrations/moadian/utils.py

535 lines
18 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.

"""
توابع کمکی برای کار با سامانه مودیان مالیاتی
"""
from __future__ import annotations
import hashlib
import re
from datetime import date, datetime
from typing import Optional, Union
DateLike = Union[datetime, date, str, None]
def coerce_to_datetime(value: DateLike) -> datetime:
"""تبدیل date/datetime/رشته ISO به datetime برای توابعی که به timestamp نیاز دارند."""
if value is None:
return datetime.utcnow()
if isinstance(value, datetime):
return value
if isinstance(value, date):
return datetime.combine(value, datetime.min.time())
if isinstance(value, str):
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
if isinstance(parsed, datetime):
return parsed
return datetime.combine(parsed, datetime.min.time())
except (TypeError, ValueError):
return datetime.utcnow()
return datetime.utcnow()
def generate_tax_id(client_id: str, timestamp: DateLike, internal_id: int) -> str:
"""
تولید شناسه یکتای مالیاتی (TAXID)
مطابق الگوریتم InvoiceIdService در کتابخانه PHP:
- clientId (tax_memory_id) + hexDaysPastEpoch (5 hex) + hexInternalInvoiceId (10 hex) + checksum
Args:
client_id: شناسه حافظه مالیاتی (tax_memory_id)
timestamp: زمان صدور فاکتور
internal_id: شناسه داخلی فاکتور
Returns:
شناسه یکتای مالیاتی (مثلا: A1B2C3000010000000001)
"""
timestamp = coerce_to_datetime(timestamp)
from app.integrations.moadian.verhoeff import verhoeff_checksum
# تبدیل clientId به عدد (مطابق clientIdToNumber در PHP)
def client_id_to_number(client_id: str) -> str:
"""تبدیل clientId به عدد - حروف به ASCII code"""
CHARACTER_TO_NUMBER_CODING = {
'A': 65, 'B': 66, 'C': 67, 'D': 68, 'E': 69, 'F': 70,
'G': 71, 'H': 72, 'I': 73, 'J': 74, 'K': 75, 'L': 76,
'M': 77, 'N': 78, 'O': 79, 'P': 80, 'Q': 81, 'R': 82,
'S': 83, 'T': 84, 'U': 85, 'V': 86, 'W': 87, 'X': 88,
'Y': 89, 'Z': 90,
}
result = ""
for char in client_id:
if char.isdigit():
result += char
elif char.upper() in CHARACTER_TO_NUMBER_CODING:
result += str(CHARACTER_TO_NUMBER_CODING[char.upper()])
return result
# محاسبه days past epoch
days_past_epoch = int(timestamp.timestamp() / (3600 * 24))
days_past_epoch_padded = str(days_past_epoch).zfill(6)
hex_days_past_epoch = hex(days_past_epoch)[2:].zfill(5).upper()
# تبدیل clientId به عدد
numeric_client_id = client_id_to_number(client_id)
# internal invoice ID
internal_id_padded = str(internal_id).zfill(12)
hex_internal_id = hex(internal_id)[2:].zfill(10).upper()
# ساخت decimal invoice ID برای checksum
decimal_invoice_id = numeric_client_id + days_past_epoch_padded + internal_id_padded
# محاسبه checksum با Verhoeff algorithm
checksum = verhoeff_checksum(decimal_invoice_id)
# ساخت شناسه نهایی: clientId + hexDays + hexInternalId + checksum
tax_id = client_id.upper() + hex_days_past_epoch + hex_internal_id + str(checksum)
return tax_id
def normalize_invoice_number(invoice_number: str | int, max_length: int = 20) -> str:
"""
نرمالایز کردن شماره فاکتور برای ارسال به سامانه
مطابق کتابخانه PHP: str_pad(dechex($internalInvoiceId), 10, 0, STR_PAD_LEFT)
Args:
invoice_number: شماره فاکتور (باید عدد باشد)
max_length: حداکثر طول مجاز (پیش‌فرض 10 برای hex)
Returns:
شماره نرمال شده به hex با padding
"""
# تبدیل به عدد صحیح
try:
if isinstance(invoice_number, str):
# حذف کاراکترهای غیرعددی
num_str = re.sub(r'[^0-9]', '', invoice_number)
if not num_str:
num_str = "0"
internal_id = int(num_str)
else:
internal_id = int(invoice_number)
except (ValueError, TypeError):
internal_id = 0
# تبدیل به hex و padding به 10 کاراکتر (مطابق کتابخانه PHP)
hex_value = hex(internal_id)[2:] # حذف '0x'
normalized = hex_value.zfill(10).upper() # padding به 10 کاراکتر و uppercase
return normalized
def timestamp_to_unix_ms(dt: DateLike) -> int:
"""
تبدیل datetime به Unix timestamp به milliseconds
Args:
dt: تاریخ و زمان
Returns:
Unix timestamp (milliseconds)
"""
return int(coerce_to_datetime(dt).timestamp() * 1000)
def datetime_to_moadian_format(dt: datetime) -> int:
"""
تبدیل datetime به فرمت عددی سامانه مودیان (yyyyMMdd)
Args:
dt: تاریخ و زمان
Returns:
عدد 8 رقمی (مثلا 20250105)
"""
return int(dt.strftime('%Y%m%d'))
def normalize_moadian_unit_code(raw: str | int | None) -> str:
"""
واحد اندازه‌گیری مودیان باید کد عددی باشد (الگوی ^\\d{0,8}$).
در داده‌های قدیمی/seed اغلب نام فارسی (مثل «عدد») ذخیره شده است.
"""
if raw is None:
return "1627" # عدد — مطابق نمونه SDK PHP
s = str(raw).strip()
if not s:
return "1627"
if s.isdigit():
return s
# یکسان‌سازی حروف عربی/فارسی
s_norm = s.replace("ي", "ی").replace("ك", "ک").replace("_", " ").strip()
# نگاشت رایج نام‌ها → کدهای سامانه مودیان
name_to_code = {
"عدد": "1627",
"کیلوگرم": "1611",
"کیلو گرم": "1611",
"گرم": "1612",
"تن": "1613",
"متر": "1620",
"سانتی متر": "1621",
"سانتی‌متر": "1621",
"متر مربع": "1622",
"متر مکعب": "1623",
"لیتر": "1628",
"میلی لیتر": "1629",
"بسته": "1630",
"جعبه": "1631",
"کارتن": "1632",
"رول": "1633",
"جفت": "1634",
"دستگاه": "1635",
"بطری": "1636",
"قوطی": "1637",
"بشکه": "1638",
"بشكه": "1638",
"گالن": "1639",
"ساعت": "1640",
"روز": "1641",
"ماه": "1642",
"سال": "1643",
"نفر": "1644",
"قرارداد": "1645",
"خدمت": "1646",
"بانکه": "1627",
"بانكه": "1627",
"برگ": "1627",
"پاکت": "1630",
"پالت": "1632",
}
return name_to_code.get(s_norm, "1627")
def calculate_vat_rate(tax_percentage: float) -> int:
"""
تبدیل درصد مالیات به فرمت سامانه مودیان / SDK PHP.
طبق مستند رسمی، vra عدد اعشاری با حداکثر ۲ رقم اعشار است (مثلاً ۹ یا ۱۰).
کتابخانه Snapp-Market و نسخهٔ قدیمی حسابیکس هم ۹٪ را به صورت ۹ می‌فرستند
(نه ۹۰۰).
ورودی می‌تواند «۹» / «۱۰» (درصد) یا «۰.۰۹» (کسر) باشد.
"""
try:
tf = float(tax_percentage)
except (TypeError, ValueError):
return 0
if tf <= 0:
return 0
# کسر اعشاری نمایندهٔ درصد (مثلاً ۰.۰۹ → ۹)؛ مقدار ۱ به‌عنوان ۱٪ حفظ می‌شود
if 0 < tf < 1.0:
tf = tf * 100.0
# مقادیر اشتباه قدیمی (×۱۰۰ مثل ۹۰۰ برای ۹٪) را به درصد واقعی برمی‌گردانیم
if tf >= 100.0:
tf = tf / 100.0
return int(round(tf))
def validate_economic_code(code: Optional[str]) -> bool:
"""
اعتبارسنجی کد اقتصادی
باید 11 یا 14 رقم باشد
Args:
code: کد اقتصادی
Returns:
True اگر معتبر باشد
"""
if not code:
return False
# حذف فاصله و خط تیره
clean = re.sub(r'[\s\-]', '', code)
# باید فقط عدد باشد
if not clean.isdigit():
return False
# طول باید 11 یا 14 رقم باشد
return len(clean) in (11, 14)
def validate_national_id(national_id: Optional[str]) -> tuple[bool, Optional[str]]:
"""
اعتبارسنجی کد ملی / شناسه ملی
- کد ملی اشخاص حقیقی: 10 رقم
- شناسه ملی اشخاص حقوقی: 11 رقم
Args:
national_id: کد/شناسه ملی
Returns:
(معتبر است؟, نوع شخص: 'natural' یا 'legal')
"""
if not national_id:
return False, None
# حذف فاصله و خط تیره
clean = re.sub(r'[\s\-]', '', national_id)
# باید فقط عدد باشد
if not clean.isdigit():
return False, None
length = len(clean)
if length == 10:
return True, 'natural' # اشخاص حقیقی
elif length == 11:
return True, 'legal' # اشخاص حقوقی
else:
return False, None
def validate_tax_code(tax_code: Optional[str]) -> bool:
"""
اعتبارسنجی کد مالیاتی کالا
باید 13 رقم باشد
Args:
tax_code: کد مالیاتی
Returns:
True اگر معتبر باشد
"""
if not tax_code:
return False
# حذف فاصله و خط تیره
clean = re.sub(r'[\s\-]', '', tax_code)
# باید فقط عدد باشد
if not clean.isdigit():
return False
# باید دقیقا 13 رقم باشد
return len(clean) == 13
def round_to_int(value: float | int | None) -> int:
"""
گرد کردن و تبدیل به عدد صحیح (سامانه اعشار قبول نمی‌کند)
Args:
value: مقدار
Returns:
عدد صحیح گرد شده
"""
if value is None:
return 0
try:
return int(round(float(value)))
except (ValueError, TypeError):
return 0
def map_invoice_type_to_moadian(document_type: str, is_return: bool = False) -> int:
"""
تبدیل نوع فاکتور داخلی به کد سامانه مودیان
Args:
document_type: نوع سند داخلی
is_return: آیا فاکتور برگشتی است؟
Returns:
کد نوع فاکتور:
- 1: فاکتور عادی
- 2: فاکتور ساده
- 3: فاکتور ابطالی
"""
# اگر برگشتی باشد، نوع ابطالی
if is_return:
return 3
# اگر خریدار دارای کد ملی و اقتصادی باشد، نوع عادی
# این منطق باید در service بررسی شود
# پیش‌فرض: ساده
return 2
def map_payment_pattern(is_cash: bool = True, is_credit: bool = False) -> int:
"""
روش تسویه (setm) در هدر صورت‌حساب مودیان.
مطابق الگوی رایج SDKها و moadian-full:
- 1: نقدی
- 2: نسیه
- 3: نقد و نسیه
"""
if is_cash and is_credit:
return 3
elif is_credit:
return 2
else:
return 1
def map_invoice_pattern(
is_return: bool = False,
is_cancel: bool = False,
is_corrective: bool = False,
) -> int:
"""
الگوی صورت‌حساب (inp): فروش / برگشت / ابطال / اصلاح.
- 1: فروش
- 2: برگشت از فروش
- 3: ابطال
- 4: اصلاحی
"""
if is_corrective:
return 4
if is_cancel:
return 3
if is_return:
return 2
return 1
def map_invoice_subject_for_inp(inp: int, document_type: str = "") -> int:
"""موضوع صورتحساب (ins) هم‌راستا با الگوی inp."""
if inp == 3:
return 3
if inp == 4:
return 4
if inp == 2:
return 5
return map_invoice_subject(document_type)
def map_invoice_subject(document_type: str) -> int:
"""
تبدیل نوع سند به موضوع فاکتور
Args:
document_type: نوع سند
Returns:
کد موضوع:
- 1: فروش
- 2: فروش ارزی
- 3: خرید
- 4: خرید ارزی
- 5: برگشت از فروش
- 6: برگشت از خرید
"""
type_lower = document_type.lower()
if 'sales' in type_lower or 'invoice_sales' in type_lower:
return 1 # فروش
elif 'return' in type_lower or 'sales_return' in type_lower:
return 5 # برگشت از فروش
elif 'purchase' in type_lower:
return 3 # خرید
else:
return 1 # پیش‌فرض: فروش
def extract_moadian_error_message(error_data: dict) -> str:
"""
استخراج پیام خطا از پاسخ سامانه مودیان
Args:
error_data: داده خطا از API
Returns:
پیام خطای فارسی
"""
if not error_data:
return "خطای نامشخص"
# نقشه کدهای خطا به پیام فارسی (بر اساس مستندات سامانه مودیان)
error_map = {
# خطاهای احراز هویت و تنظیمات
'TAX-101': 'شناسه حافظه مالیاتی نامعتبر است',
'TAX-102': 'کد اقتصادی نامعتبر است',
'TAX-103': 'کلید خصوصی نامعتبر است',
'TAX-104': 'امضای دیجیتال نامعتبر است',
'AUTH-001': 'خطا در احراز هویت',
'AUTH-002': 'توکن منقضی شده است',
'AUTH-003': 'دسترسی غیرمجاز',
'AUTH-004': 'شناسه حافظه مالیاتی یافت نشد',
'4103': (
'کد ملی گواهی امضا با شناسه کلاینت توکن مطابقت ندارد. '
'اگر در نسخه قبلی (PHP) فقط با کلید خصوصی ارسال می‌کردید، فیلد «گواهی» را خالی کنید '
'یا گواهی صادرشده از CSR همین کلید را بارگذاری کنید.'
),
'TAX_CERTIFICATE_REQUIRED': 'گواهی امضای الکترونیک در تنظیمات ثبت نشده است',
'TAX_CERTIFICATE_INVALID': 'گواهی امضا نامعتبر است یا توسط مرکز میانی صادر نشده',
# خطاهای اعتبارسنجی فاکتور
'TAX-201': 'فاکتور تکراری است',
'TAX-202': 'شماره فاکتور نامعتبر است',
'TAX-203': 'تاریخ فاکتور نامعتبر است',
'TAX-204': 'نوع فاکتور نامعتبر است',
'TAX-205': 'الگوی پرداخت نامعتبر است',
'TAX-206': 'موضوع فاکتور نامعتبر است',
# خطاهای اقلام فاکتور
'TAX-301': 'کد مالیاتی کالا نامعتبر است',
'TAX-302': 'کد ملی خریدار نامعتبر است',
'TAX-303': 'مبلغ فاکتور نامعتبر است',
'TAX-304': 'واحد اندازه‌گیری نامعتبر است',
'TAX-305': 'تعداد/مقدار کالا نامعتبر است',
'TAX-306': 'نرخ مالیات نامعتبر است',
'TAX-307': 'مبلغ مالیات نامعتبر است',
# خطاهای شبکه و ارتباط
'TAX-401': 'خطا در اتصال به سرور',
'TAX-402': 'زمان انتظار پاسخ تمام شد',
'TAX-403': 'سرور در دسترس نیست',
'TAX-404': 'مسیر درخواست یافت نشد',
# خطاهای پردازش
'TAX-501': 'خطا در پردازش درخواست',
'TAX-502': 'داده‌های ارسالی ناقص است',
'TAX-503': 'سرویس موقتاً در دسترس نیست',
# خطاهای عمومی
'TAX-999': 'خطای نامشخص از سامانه مودیان',
}
# استخراج error_code و error_message از error_data
error_code = ''
error_message = ''
if isinstance(error_data, dict):
error_code = error_data.get('code', '') or error_data.get('errorCode', '')
error_message = error_data.get('message', '') or error_data.get('errorMessage', '') or error_data.get('detail', '')
elif isinstance(error_data, str):
error_message = error_data
# نرمالایز کردن error_code
if error_code:
error_code = str(error_code).strip().upper()
# اگر کد خطا در نقشه بود
if error_code and error_code in error_map:
mapped_message = error_map[error_code]
if error_message and error_message != mapped_message:
return f"{mapped_message}: {error_message} ({error_code})"
return f"{mapped_message} ({error_code})"
# اگر پیام خطا موجود بود
if error_message:
error_message_str = str(error_message).strip()
if error_code:
return f"{error_message_str} ({error_code})"
return error_message_str
# در غیر این صورت کد خطا را نمایش می‌دهیم
if error_code:
return f"خطا: {error_code}"
return "خطای نامشخص از سامانه مودیان"