Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/app/services/tax_validation_service.py

273 lines
12 KiB
Python
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
from typing import Dict, Any, List
from sqlalchemy.orm import Session
from adapters.db.models.person import Person
from adapters.db.models.product import Product
from adapters.db.models.invoice_item_line import InvoiceItemLine
from app.integrations.moadian.utils import (
validate_tax_code,
)
def validate_document_for_tax(db: Session, document) -> Dict[str, Any]:
"""
بررسی کامل داده‌های لازم برای ارسال فاکتور به سامانه مودیان.
بررسی‌ها:
- وجود طرف حساب (برای ثبت حسابداری فاکتور)
- کد مالیاتی کالاها (13 رقم)
- واحد مالیاتی کالاها
- صحیح بودن مبالغ مالیات (بدون اعشار)
- محدودیت‌های دیگر
شناسه مالیاتی خریدار (کد ملی / کد اقتصادی) الزامی نیست:
در صورت نبود هویت کامل خریدار، سازنده فاکتور آن را به‌صورت نوع ۲
(مصرف‌کننده نهایی، بدون فیلد خریدار) به مودیان می‌فرستد.
کدهای نامعتبر نیز در builder نادیده گرفته می‌شوند و به نوع ۲ می‌انجامند؛
اینجا بلاک نمی‌شوند تا با رفتار سامانه و builder هم‌خوان باشد.
Returns:
{
"valid": bool,
"issues": [{"code": str, "message": str, "meta": dict}]
}
"""
issues: List[Dict[str, Any]] = []
extra = document.extra_info or {}
# 1. بررسی اطلاعات شخص (وجود طرف‌حساب؛ نه الزام شناسه مالیاتی)
person_id = extra.get("person_id")
person: Person | None = None
if not person_id:
issues.append({
"code": "PERSON_MISSING",
"message": "برای فاکتورهای فروش/خرید، انتخاب طرف حساب الزامی است.",
})
else:
person = db.query(Person).filter(Person.id == int(person_id)).first()
if not person:
issues.append({
"code": "PERSON_NOT_FOUND",
"message": "طرف حساب انتخاب شده یافت نشد.",
})
# else: کد ملی/اقتصادی اختیاری است → نوع ۱ یا ۲ در invoice_builder تعیین می‌شود
# 2. بررسی اقلام فاکتور
item_lines: List[InvoiceItemLine] = (
db.query(InvoiceItemLine)
.filter(InvoiceItemLine.document_id == document.id)
.all()
)
if not item_lines:
issues.append({
"code": "LINES_MISSING",
"message": "فاکتور فاقد ردیف کالا/خدمت است.",
})
for idx, line in enumerate(item_lines, 1):
product = db.query(Product).filter(Product.id == line.product_id).first()
if not product:
issues.append({
"code": "PRODUCT_NOT_FOUND",
"message": f"کالای ردیف {idx} حذف شده است.",
"meta": {"line_id": line.id, "line_number": idx},
})
continue
# بررسی کد مالیاتی
tax_code = (product.tax_code or "").strip()
if not tax_code:
issues.append({
"code": "PRODUCT_TAX_CODE_MISSING",
"message": f"کالای '{product.name}' (ردیف {idx}) فاقد کد مالیاتی است.",
"meta": {"product_id": product.id, "product_name": product.name, "line_number": idx},
})
elif not validate_tax_code(tax_code):
issues.append({
"code": "PRODUCT_TAX_CODE_INVALID",
"message": f"کد مالیاتی کالای '{product.name}' (ردیف {idx}) نامعتبر است (باید دقیقا 13 رقم باشد).",
"meta": {"product_id": product.id, "tax_code": tax_code, "line_number": idx},
})
# بررسی واحد مالیاتی
if not product.tax_unit_id:
issues.append({
"code": "PRODUCT_TAX_UNIT_MISSING",
"message": f"کالای '{product.name}' (ردیف {idx}) فاقد واحد مالیاتی است.",
"meta": {"product_id": product.id, "product_name": product.name, "line_number": idx},
})
# بررسی مالیات (نباید اعشار داشته باشد)
line_extra = line.extra_info or {}
tax_amount = line_extra.get("tax_amount", 0)
try:
tax_float = float(tax_amount)
# بررسی اعشار
if tax_float != int(tax_float):
issues.append({
"code": "TAX_AMOUNT_HAS_DECIMAL",
"message": f"مبلغ مالیات ردیف {idx} (کالای '{product.name}') نباید اعشار داشته باشد.",
"meta": {"line_id": line.id, "tax_amount": tax_amount, "line_number": idx},
})
# بررسی منفی نبودن
if tax_float < 0:
issues.append({
"code": "TAX_AMOUNT_NEGATIVE",
"message": f"مبلغ مالیات ردیف {idx} (کالای '{product.name}') نمی‌تواند منفی باشد.",
"meta": {"line_id": line.id, "tax_amount": tax_amount, "line_number": idx},
})
except (ValueError, TypeError):
issues.append({
"code": "TAX_AMOUNT_INVALID",
"message": f"مبلغ مالیات ردیف {idx} نامعتبر است.",
"meta": {"line_id": line.id, "line_number": idx},
})
# 3. بررسی هزینه حمل (اختیاری - اگر وجود داشته باشد باید معتبر باشد)
shipping_cost = (extra or {}).get("shipping_cost", 0)
try:
shipping_cost_val = float(shipping_cost or 0)
# اگر هزینه حمل منفی باشد، خطا می‌دهیم
if shipping_cost_val < 0:
issues.append({
"code": "SHIPPING_COST_NEGATIVE",
"message": "هزینه حمل نمی‌تواند منفی باشد.",
"meta": {"shipping_cost": shipping_cost},
})
# اگر هزینه حمل اعشار داشته باشد، خطا می‌دهیم (سامانه اعشار قبول نمی‌کند)
elif shipping_cost_val > 0 and shipping_cost_val != int(shipping_cost_val):
issues.append({
"code": "SHIPPING_COST_HAS_DECIMAL",
"message": "هزینه حمل نباید اعشار داشته باشد.",
"meta": {"shipping_cost": shipping_cost},
})
except (ValueError, TypeError):
if shipping_cost: # فقط اگر مقدار غیر صفر باشد خطا می‌دهیم
issues.append({
"code": "SHIPPING_COST_INVALID",
"message": "مبلغ هزینه حمل نامعتبر است.",
"meta": {"shipping_cost": shipping_cost},
})
# 4. بررسی مبلغ کل فاکتور
totals = extra.get("totals") or {}
total_amount = totals.get("net", 0)
try:
total_float = float(total_amount)
if total_float <= 0:
issues.append({
"code": "TOTAL_AMOUNT_INVALID",
"message": "مبلغ کل فاکتور باید بیشتر از صفر باشد.",
"meta": {"total_amount": total_amount},
})
except (ValueError, TypeError):
issues.append({
"code": "TOTAL_AMOUNT_INVALID",
"message": "مبلغ کل فاکتور نامعتبر است.",
})
return {
"valid": len(issues) == 0,
"issues": issues,
"total_issues": len(issues),
}
def validate_tax_submission_scenario(
db: Session,
document,
tax_setting,
submission_mode: str | None = None,
) -> Dict[str, Any]:
"""اعتبارسنجی سناریوی ارسال (فروش / برگشت / ابطال / اصلاح)."""
from app.services.tax_reference_service import (
resolve_irtaxid,
resolve_reference_document,
is_modian_submitted,
get_document_extra,
compute_taxid_for_document,
TAX_SENT_STATUSES,
)
issues: List[Dict[str, Any]] = []
extra = get_document_extra(document)
mode = (submission_mode or "").strip().lower()
doc_type = (document.document_type or "").lower()
if mode == "cancel":
mode = "cancel"
elif mode == "corrective":
pass
elif "return" in doc_type or mode == "return":
mode = "return"
else:
mode = "normal"
if mode == "return":
ref_doc = resolve_reference_document(db, document.business_id, document)
if ref_doc is None:
issues.append({
"code": "TAX_REFERENCE_INVOICE_MISSING",
"message": "برای برگشت از فروش، فاکتور مرجع (فروش اصلی) باید مشخص شود.",
})
else:
ref_extra = get_document_extra(ref_doc)
if not is_modian_submitted(ref_extra):
issues.append({
"code": "TAX_REFERENCE_NOT_SENT",
"message": "فاکتور مرجع هنوز در سامانه مودیان ارسال/قطعی نشده است.",
"meta": {"reference_invoice_id": ref_doc.id, "reference_code": ref_doc.code},
})
irtaxid = resolve_irtaxid(db, document.business_id, document, tax_setting)
if not irtaxid:
issues.append({
"code": "TAX_REFERENCE_TAXID_MISSING",
"message": "شناسه مالیاتی (taxid) فاکتور مرجع قابل محاسبه نیست.",
"meta": {"reference_invoice_id": ref_doc.id},
})
elif mode == "cancel":
status = str(extra.get("tax_status") or "").strip().lower()
if status not in TAX_SENT_STATUSES and not extra.get("tax_tracking_code"):
issues.append({
"code": "TAX_CANCEL_NOT_SENT",
"message": "فقط فاکتورهای ارسال‌شده به مودیان قابل ابطال در سامانه هستند.",
})
if extra.get("tax_cancelled_in_modian"):
issues.append({
"code": "TAX_ALREADY_CANCELLED",
"message": "این فاکتور قبلاً در سامانه مودیان ابطال شده است.",
})
elif mode == "corrective":
status = str(extra.get("tax_status") or "").strip().lower()
if status not in TAX_SENT_STATUSES and not extra.get("tax_tracking_code"):
issues.append({
"code": "TAX_CORRECTIVE_REQUIRES_SENT",
"message": "صورتحساب اصلاحی فقط پس از ارسال اولیه مجاز است.",
})
irtaxid = resolve_irtaxid(db, document.business_id, document, tax_setting)
if not irtaxid:
irtaxid = compute_taxid_for_document(document, tax_setting)
if not irtaxid:
issues.append({
"code": "TAX_CORRECTIVE_TAXID_MISSING",
"message": "شناسه مالیاتی مرجع برای اصلاح یافت نشد.",
})
return {
"valid": len(issues) == 0,
"issues": issues,
"submission_mode": mode,
}