diff --git a/backend/app/services/project_transfer_service.py b/backend/app/services/project_transfer_service.py index 1bdf617..da002cf 100644 --- a/backend/app/services/project_transfer_service.py +++ b/backend/app/services/project_transfer_service.py @@ -100,6 +100,56 @@ async def create_project_transfer( specs_hash, for_update=True, ) + + # Smart fallback: If exact hash or brand didn't match, check candidate rows in this warehouse for this material + if source_item is None: + candidates_res = await db.execute( + select(InventoryItem) + .where( + InventoryItem.warehouse_id == payload.from_warehouse_id, + InventoryItem.material_id == payload.material_id, + ) + .with_for_update() + ) + candidates = candidates_res.scalars().all() + + if len(candidates) == 1: + source_item = candidates[0] + elif len(candidates) > 1: + best_cand = None + best_score = -1 + p_keys = set(specs.keys()) + p_vals = set(str(v).strip() for v in specs.values() if str(v).strip()) + + for cand in candidates: + score = 0 + if cand.brand_id == payload.brand_id: + score += 10 + elif cand.brand_id is None and payload.brand_id is None: + score += 10 + elif cand.brand_id is None: + score += 4 + + c_specs = cand.specs or {} + c_keys = set(c_specs.keys()) + c_vals = set(str(v).strip() for v in c_specs.values() if str(v).strip()) + + for pk in p_keys: + if pk in c_keys or f"spec_{pk}" in c_keys or any(pk in ck for ck in c_keys): + score += 5 + for pv in p_vals: + if pv in c_vals or any(pv in str(cv) for cv in c_vals): + score += 5 + + if cand.quantity >= payload.quantity: + score += 2 + + if score > best_score: + best_score = score + best_cand = cand + + source_item = best_cand + if source_item is None or source_item.quantity < payload.quantity: avail = source_item.quantity if source_item else Decimal("0") raise ValidationError( @@ -129,16 +179,20 @@ async def create_project_transfer( db.add(out_tx) await db.flush() + actual_brand_id = source_item.brand_id + actual_specs = source_item.specs if source_item.specs is not None else specs + actual_specs_hash = source_item.specs_hash or canonical_specs_hash(actual_specs) + transfer = ProjectTransfer( from_project_id=from_wh.project_id, from_warehouse_id=from_wh.id, to_project_id=payload.to_project_id, material_id=payload.material_id, - brand_id=payload.brand_id, - unit_id=payload.unit_id, + brand_id=actual_brand_id, + unit_id=source_item.unit_id if source_item.unit_id else payload.unit_id, quantity=payload.quantity, - specs=specs, - specs_hash=specs_hash, + specs=actual_specs, + specs_hash=actual_specs_hash, note=payload.note, status="in_transit", out_transaction_id=out_tx.id, diff --git a/frontend/src/pages/Inventory/ChangeInventoryPage.tsx b/frontend/src/pages/Inventory/ChangeInventoryPage.tsx index 6bdf628..9a03f2f 100644 --- a/frontend/src/pages/Inventory/ChangeInventoryPage.tsx +++ b/frontend/src/pages/Inventory/ChangeInventoryPage.tsx @@ -238,17 +238,12 @@ export function ChangeInventoryPage() { } }, [type, loadTransfers]); - const matchedStock = useMemo(() => { - if (type !== "PROJECT_TRANSFER" || !whId || !matTxt.trim()) return null; - const mt = matTxt.trim(); - const bt = brandTxt.trim(); - return data.inventory.filter((r) => { - if (r.warehouse?.id !== whId) return false; - if (r.material.name !== mt) return false; - if (bt && r.brand?.name !== bt) return false; - return true; - }); - }, [type, whId, matTxt, brandTxt, data.inventory]); + const whStock = useMemo(() => { + if (!whId) return []; + return (data.inventory || []).filter((r) => r.warehouse?.id === whId && r.quantity_number > 0); + }, [data.inventory, whId]); + + const handleSaveEdit = async () => { if (!editingTransfer) return; @@ -340,36 +335,52 @@ export function ChangeInventoryPage() { if (!whId) return toast.error("انبار مبدأ را انتخاب کنید"); if (!toProjectId) return toast.error("پروژه مقصد را انتخاب کنید"); if (!matName) return toast.error("نام متریال را وارد کنید"); - if (!brandName) return toast.error("نام برند را وارد کنید"); if (!unitName) return toast.error("نام واحد را وارد کنید"); if (!(n > 0)) return toast.error("تعداد باید بزرگتر از صفر باشد"); - if (!specsComplete) return toast.error("حداقل یک مشخصه فنی ثبت کنید"); setBusy(true); try { - let uid = unitIdByName(unitName); + // Find matching inventory in the selected source warehouse + const invInWh = (data.inventory || []).filter( + (r) => r.warehouse?.id === whId && r.material.name.trim() === matName + ); + const matchedInv = invInWh.find( + (r) => (brandName ? r.brand?.name === brandName : !r.brand) + ) || invInWh[0]; + + let uid = matchedInv?.unit?.id ?? unitIdByName(unitName); if (!uid) { const created = await apiV26.catalog.createUnit(unitName); uid = created.id; } - let bid = brandIdByName(brandName); - if (!bid) { + + let bid: string | null = matchedInv?.brand?.id ?? (brandName ? brandIdByName(brandName) : null); + if (brandName && !bid) { const created = await apiV26.catalog.createBrand(brandName); bid = created.id; } - let m = matByName(matName); - if (!m) { - const created = await apiV26.catalog.createMaterial({ name: matName, canonical_unit_id: uid, brand_ids: bid ? [bid] : [] }); - m = { id: created.id, name: created.name, canonical_unit: null }; + + let mid = matchedInv?.material.id; + if (!mid) { + let m = matByName(matName); + if (!m) { + const created = await apiV26.catalog.createMaterial({ name: matName, canonical_unit_id: uid, brand_ids: bid ? [bid] : [] }); + mid = created.id; + } else { + mid = m.id; + } } + + const finalSpecs = matchedInv ? (matchedInv.specs || {}) : specsFromChips(); + await apiV26.projectTransfers.create({ from_warehouse_id: whId, to_project_id: toProjectId, - material_id: m.id, - brand_id: bid, + material_id: mid, + brand_id: bid || null, unit_id: uid, quantity: n, - specs: specsFromChips(), + specs: finalSpecs, note, transfer_date: toServerIso(), }, user?.project_id); @@ -530,7 +541,7 @@ export function ChangeInventoryPage() { return !!(whId && matTxt.trim() && brandTxt.trim() && unitTxt.trim() && parseNum(qty) > 0 && specsComplete); } if (type === "PROJECT_TRANSFER") { - return !!(whId && toProjectId && matTxt.trim() && brandTxt.trim() && unitTxt.trim() && parseNum(qty) > 0 && specsComplete); + return !!(whId && toProjectId && matTxt.trim() && unitTxt.trim() && parseNum(qty) > 0); } if (type === "TRANSFER") return !!(fromId && toId && matTxt.trim() && rows.some((r) => parseNum(r.qty) > 0)); if (type === "RETURN") return !!(whId && matTxt.trim() && rows.some((r) => parseNum(r.qty) > 0)); @@ -766,6 +777,69 @@ export function ChangeInventoryPage() { {/* فرم خرید / موجودی اولیه یا انتقال بین کارگاهی */} {(type === "PURCHASE" || type === "PROJECT_TRANSFER") && (