forked from hesabix/arc
277 lines
11 KiB
Python
Executable file
277 lines
11 KiB
Python
Executable file
"""
|
|
ایجاد/ویرایش گروهی فاکتورها برای ادغام (مثل چند سفارش ووکامرس در یک تماس شبکه از سمت افزونه).
|
|
هر آیتم در تراکنش/منطق جدا؛ خطای یک آیتم بقیه را متوقف نمیکند.
|
|
"""
|
|
|
|
from typing import Any, Dict, List
|
|
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
from adapters.db.models.document import Document
|
|
from app.core.auth_dependency import AuthContext
|
|
from app.core.permissions import (
|
|
has_business_permission_for_business,
|
|
has_invoice_type_permission_for_business,
|
|
)
|
|
from app.core.responses import ApiError
|
|
from app.services.invoice_service import create_invoice, update_invoice, SUPPORTED_INVOICE_TYPES
|
|
|
|
MAX_BULK_INVOICE_ITEMS = 1000
|
|
|
|
|
|
def _api_error_parts(err: ApiError) -> Dict[str, Any]:
|
|
d = getattr(err, "detail", None)
|
|
if isinstance(d, dict):
|
|
payload = d.get("error") or {}
|
|
return {
|
|
"code": str(payload.get("code") or "API_ERROR"),
|
|
"message": str(payload.get("message") or str(err)),
|
|
}
|
|
return {"code": "API_ERROR", "message": str(err)}
|
|
|
|
|
|
def bulk_upsert_invoices_integration(
|
|
db: Session,
|
|
business_id: int,
|
|
auth_context: AuthContext,
|
|
body: Dict[str, Any],
|
|
*,
|
|
user_can_select_fx_rate: bool,
|
|
) -> Dict[str, Any]:
|
|
items = body.get("items")
|
|
if not isinstance(items, list):
|
|
raise ApiError("INVALID_REQUEST", "items باید آرایه باشد", http_status=400)
|
|
|
|
if len(items) > MAX_BULK_INVOICE_ITEMS:
|
|
raise ApiError(
|
|
"BULK_TOO_LARGE",
|
|
f"حداکثر {MAX_BULK_INVOICE_ITEMS} آیتم در هر درخواست مجاز است",
|
|
http_status=400,
|
|
)
|
|
|
|
migration_mode = bool(body.get("migration_mode"))
|
|
token = None
|
|
if migration_mode:
|
|
if not has_business_permission_for_business(auth_context, db, business_id, "invoices", "add"):
|
|
raise ApiError("FORBIDDEN", "مجوز مهاجرت فاکتور وجود ندارد", http_status=403)
|
|
from app.services.legacy_import.context import set_legacy_import_active
|
|
|
|
token = set_legacy_import_active(True)
|
|
|
|
uid = auth_context.get_user_id()
|
|
results: List[Dict[str, Any]] = []
|
|
|
|
try:
|
|
for idx, raw in enumerate(items):
|
|
base = {"index": idx}
|
|
cref = raw.get("client_ref") if isinstance(raw, dict) else None
|
|
if cref is not None:
|
|
base["client_ref"] = str(cref).strip() or None
|
|
else:
|
|
base["client_ref"] = None
|
|
|
|
if not isinstance(raw, dict):
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "failed",
|
|
"invoice_id": None,
|
|
"error_code": "INVALID_ITEM",
|
|
"message": "آیتم باید شیٔ JSON باشد",
|
|
}
|
|
)
|
|
continue
|
|
|
|
payload = raw.get("payload")
|
|
if not isinstance(payload, dict):
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "failed",
|
|
"invoice_id": None,
|
|
"error_code": "INVALID_PAYLOAD",
|
|
"message": "payload الزامی است و باید شیٔ باشد",
|
|
}
|
|
)
|
|
continue
|
|
|
|
inv_type = str(payload.get("invoice_type") or "").strip()
|
|
|
|
raw_iid = raw.get("invoice_id")
|
|
invoice_id_val: int | None = None
|
|
if raw_iid not in (None, ""):
|
|
try:
|
|
invoice_id_val = int(raw_iid)
|
|
except (ValueError, TypeError):
|
|
invoice_id_val = None
|
|
if invoice_id_val is not None and invoice_id_val <= 0:
|
|
invoice_id_val = None
|
|
|
|
# --- ایجاد ---
|
|
if invoice_id_val is None:
|
|
if not has_business_permission_for_business(auth_context, db, business_id, "invoices", "add"):
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "failed",
|
|
"invoice_id": None,
|
|
"error_code": "FORBIDDEN",
|
|
"message": "مجوز افزودن فاکتور وجود ندارد",
|
|
}
|
|
)
|
|
continue
|
|
if inv_type not in SUPPORTED_INVOICE_TYPES:
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "failed",
|
|
"invoice_id": None,
|
|
"error_code": "INVALID_INVOICE_TYPE",
|
|
"message": f"نوع فاکتور نامعتبر: {inv_type!r}",
|
|
}
|
|
)
|
|
continue
|
|
if not has_invoice_type_permission_for_business(
|
|
auth_context, db, business_id, inv_type, "add"
|
|
):
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "failed",
|
|
"invoice_id": None,
|
|
"error_code": "FORBIDDEN",
|
|
"message": f"مجوز ثبت برای نوع سند ({inv_type}) وجود ندارد",
|
|
}
|
|
)
|
|
continue
|
|
try:
|
|
doc_dict = create_invoice(
|
|
db=db,
|
|
business_id=business_id,
|
|
user_id=int(uid),
|
|
data=payload,
|
|
user_can_select_fx_rate=user_can_select_fx_rate,
|
|
)
|
|
nid = doc_dict.get("id") if isinstance(doc_dict, dict) else None
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "created",
|
|
"invoice_id": int(nid) if nid is not None else None,
|
|
}
|
|
)
|
|
except ApiError as ae:
|
|
p = _api_error_parts(ae)
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "failed",
|
|
"invoice_id": None,
|
|
"error_code": p["code"],
|
|
"message": p["message"],
|
|
}
|
|
)
|
|
except IntegrityError as e:
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "failed",
|
|
"invoice_id": None,
|
|
"error_code": "INTEGRITY_ERROR",
|
|
"message": str(getattr(e, "orig", e) or e),
|
|
}
|
|
)
|
|
|
|
else:
|
|
# --- ویرایش ---
|
|
if not has_business_permission_for_business(auth_context, db, business_id, "invoices", "edit"):
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "failed",
|
|
"invoice_id": invoice_id_val,
|
|
"error_code": "FORBIDDEN",
|
|
"message": "مجوز ویرایش فاکتور وجود ندارد",
|
|
}
|
|
)
|
|
continue
|
|
|
|
doc = db.query(Document).filter(Document.id == invoice_id_val).first()
|
|
if not doc or doc.business_id != business_id or doc.document_type not in SUPPORTED_INVOICE_TYPES:
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "failed",
|
|
"invoice_id": invoice_id_val,
|
|
"error_code": "DOCUMENT_NOT_FOUND",
|
|
"message": "فاکتور یافت نشد یا نامعتبر است",
|
|
}
|
|
)
|
|
continue
|
|
|
|
if not has_invoice_type_permission_for_business(
|
|
auth_context, db, business_id, doc.document_type, "edit"
|
|
):
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "failed",
|
|
"invoice_id": invoice_id_val,
|
|
"error_code": "FORBIDDEN",
|
|
"message": f"مجوز ویرایش برای نوع سند ({doc.document_type}) وجود ندارد",
|
|
}
|
|
)
|
|
continue
|
|
|
|
try:
|
|
doc_dict = update_invoice(
|
|
db=db,
|
|
document_id=invoice_id_val,
|
|
user_id=int(uid),
|
|
data=payload,
|
|
user_can_select_fx_rate=user_can_select_fx_rate,
|
|
)
|
|
nid = doc_dict.get("id") if isinstance(doc_dict, dict) else None
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "updated",
|
|
"invoice_id": int(nid) if nid is not None else invoice_id_val,
|
|
}
|
|
)
|
|
except ApiError as ae:
|
|
p = _api_error_parts(ae)
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "failed",
|
|
"invoice_id": invoice_id_val,
|
|
"error_code": p["code"],
|
|
"message": p["message"],
|
|
}
|
|
)
|
|
except IntegrityError as e:
|
|
results.append(
|
|
{
|
|
**base,
|
|
"status": "failed",
|
|
"invoice_id": invoice_id_val,
|
|
"error_code": "INTEGRITY_ERROR",
|
|
"message": str(getattr(e, "orig", e) or e),
|
|
}
|
|
)
|
|
finally:
|
|
if token is not None:
|
|
from app.services.legacy_import.context import reset_legacy_import_active
|
|
|
|
reset_legacy_import_active(token)
|
|
|
|
summary = {
|
|
"total": len(items),
|
|
"created": sum(1 for r in results if r.get("status") == "created"),
|
|
"updated": sum(1 for r in results if r.get("status") == "updated"),
|
|
"failed": sum(1 for r in results if r.get("status") == "failed"),
|
|
}
|
|
|
|
return {"results": results, "summary": summary}
|