request = $request; } /** * Display the Maker-Checker Execution page. */ public function index(Request $request) { $fromDate = $request->get('from_date'); $toDate = $request->get('to_date'); $businessName = $request->get('business_name'); $transactionType = $request->get('transaction_type'); $status = $request->get('status'); $search = $request->get('search'); $query = OdooExecution::query(); if ($fromDate) { $query->whereDate('initiated_at', '>=', $fromDate); } if ($toDate) { $query->whereDate('initiated_at', '<=', $toDate); } if ($businessName) { $query->where(function ($q) use ($businessName) { $q->where('business_name', $businessName) ->orWhere('system_slug', $businessName); }); } if ($transactionType) { $query->where('transaction_type', $transactionType); } if ($search) { $query->where(function ($q) use ($search) { $q->where('loan_no', 'like', "%{$search}%") ->orWhere('customer_name', 'like', "%{$search}%") ->orWhere('transaction_number', 'like', "%{$search}%"); }); } if ($status) { if ($status === 'pending_l1') { $query->where('l1_status', 'pending'); } elseif ($status === 'pending_l2') { $query->where('l1_status', 'approved')->where('l2_status', 'pending'); } elseif ($status === 'ready') { $query->where('odoo_status', 'ready'); } elseif ($status === 'pushed') { $query->where('odoo_status', 'pushed'); } elseif ($status === 'failed') { $query->where('odoo_status', 'failed'); } elseif ($status === 'rejected') { $query->where(function ($q) { $q->where('l1_status', 'rejected')->orWhere('l2_status', 'rejected'); }); } } $executions = $query->orderBy('id', 'desc')->paginate(20); // Fetch dynamic filter choices $systems = OdooSystem::where('is_active', true)->orderBy('name')->get(); $sections = OdooJvSection::select('name')->distinct()->orderBy('name')->pluck('name'); $user = Auth::user(); $odooRole = $user ? $user->odoo_role : null; $isMaker = $user && $user->isOdooMaker(); $isCheckerL1 = $user && $user->isOdooCheckerL1(); $isCheckerL2 = $user && $user->isOdooCheckerL2(); return view('admin.odoo.execution', compact( 'executions', 'systems', 'sections', 'fromDate', 'toDate', 'businessName', 'transactionType', 'status', 'search', 'isMaker', 'isCheckerL1', 'isCheckerL2', 'odooRole' )); } /** * Retrieve full details of an execution record for the side drawer. */ public function getDetails($id) { $execution = OdooExecution::find($id); if (!$execution) { return response()->json([ 'status' => 'error', 'message' => 'Execution record not found' ], 404); } // Extract human-readable error message from odoo_response if push failed $errorMessage = null; if ($execution->odoo_status === 'failed' && !empty($execution->odoo_response)) { $raw = $execution->odoo_response; $decoded = json_decode($raw, true); if (is_array($decoded)) { $errorMessage = $decoded['message'] ?? $decoded['error'] ?? $decoded['faultstring'] ?? null; if (!$errorMessage && isset($decoded['error']) && is_array($decoded['error'])) { $errorMessage = $decoded['error']['message'] ?? $decoded['error']['data']['message'] ?? json_encode($decoded['error']); } } if (!$errorMessage) { if (preg_match('/(.*?)<\/title>/is', $raw, $titleMatches)) { $title = trim(strip_tags($titleMatches[1])); if (preg_match('/<p>(.*?)<\/p>/is', $raw, $pMatches)) { $pText = trim(strip_tags($pMatches[1])); $errorMessage = $title . ': ' . $pText; } elseif (preg_match('/<h1>(.*?)<\/h1>/is', $raw, $h1Matches)) { $h1Text = trim(strip_tags($h1Matches[1])); $errorMessage = $title . ($h1Text !== $title ? (' - ' . $h1Text) : ''); } else { $errorMessage = $title; } } else { $errorMessage = trim(strip_tags($raw)); } } } return response()->json([ 'status' => 'success', 'data' => [ 'id' => $execution->id, 'application_id' => $execution->application_id, 'loan_no' => $execution->loan_no, 'customer_name' => $execution->customer_name, 'business_name' => $execution->business_name, 'system_slug' => $execution->system_slug, 'transaction_type' => $execution->transaction_type, 'step_key' => $execution->step_key, 'voucher_type' => $execution->voucher_type, 'month' => $execution->month, 'year' => $execution->year, 'total_debit' => $execution->total_debit, 'total_credit' => $execution->total_credit, 'amount' => $execution->amount, 'entries' => $execution->entries, 'maker_id' => $execution->maker_id, 'maker_name' => $execution->maker_name, 'initiated_at' => $execution->initiated_at ? $execution->initiated_at->format('d M Y, h:i A') : '—', 'l1_status' => $execution->l1_status, 'l1_user_name' => $execution->l1_user_name, 'l1_approved_at' => $execution->l1_approved_at ? $execution->l1_approved_at->format('d M Y, h:i A') : '—', 'l1_remarks' => $execution->l1_remarks, 'l2_status' => $execution->l2_status, 'l2_user_name' => $execution->l2_user_name, 'l2_approved_at' => $execution->l2_approved_at ? $execution->l2_approved_at->format('d M Y, h:i A') : '—', 'l2_remarks' => $execution->l2_remarks, 'odoo_status' => $execution->odoo_status, 'transaction_number' => $execution->transaction_number, 'odoo_move_id' => $execution->odoo_move_id, 'odoo_voucher_number'=> $execution->odoo_voucher_number, 'odoo_response' => $execution->odoo_response, 'odoo_error_message' => $errorMessage, 'pushed_at' => $execution->pushed_at ? $execution->pushed_at->format('d M Y, h:i A') : '—', ] ]); } /** * L1 Checker Approval. */ public function approveL1($id, Request $request) { $execution = OdooExecution::find($id); if (!$execution) { return response()->json(['status' => 'error', 'message' => 'Execution record not found'], 404); } if ($execution->l1_status !== 'pending') { return response()->json(['status' => 'error', 'message' => 'L1 status is already ' . $execution->l1_status], 400); } $user = Auth::user(); if (!$user || !$user->isOdooCheckerL1()) { return response()->json([ 'status' => 'error', 'message' => 'Unauthorized: Only an assigned Odoo L1 Checker can approve L1.' ], 403); } $userId = $user->id; $userName = $user->name ?? $user->email; if ($execution->maker_id == $userId) { return response()->json([ 'status' => 'error', 'message' => 'Maker cannot approve their own initiated entry.' ], 403); } $execution->l1_status = 'approved'; $execution->l1_user_id = $userId; $execution->l1_user_name = $userName; $execution->l1_approved_at = Carbon::now(); $execution->l1_remarks = $request->get('remarks', 'Approved by L1'); $execution->save(); return response()->json([ 'status' => 'success', 'message' => 'Approved by L1 Checker. Now awaiting L2 Checker approval.', 'data' => $execution ]); } /** * L2 Checker Approval (Final Approval -> sets odoo_status to 'ready'). */ public function approveL2($id, Request $request) { $execution = OdooExecution::find($id); if (!$execution) { return response()->json(['status' => 'error', 'message' => 'Execution record not found'], 404); } if ($execution->l1_status !== 'approved') { return response()->json(['status' => 'error', 'message' => 'L1 approval is required before L2 approval'], 400); } if ($execution->l2_status !== 'pending') { return response()->json(['status' => 'error', 'message' => 'L2 status is already ' . $execution->l2_status], 400); } $user = Auth::user(); if (!$user || !$user->isOdooCheckerL2()) { return response()->json([ 'status' => 'error', 'message' => 'Unauthorized: Only an assigned Odoo L2 Checker can approve L2.' ], 403); } $userId = $user->id; $userName = $user->name ?? $user->email; if ($execution->maker_id == $userId) { return response()->json([ 'status' => 'error', 'message' => 'Maker cannot approve their own initiated entry.' ], 403); } if ($execution->l1_user_id == $userId) { return response()->json([ 'status' => 'error', 'message' => 'The same checker cannot approve both L1 and L2 for this entry.' ], 403); } $execution->l2_status = 'approved'; $execution->l2_user_id = $userId; $execution->l2_user_name = $userName; $execution->l2_approved_at = Carbon::now(); $execution->l2_remarks = $request->get('remarks', 'Approved by L2'); // Once final L2 is approved, Odoo column shows Ready $execution->odoo_status = 'ready'; $execution->save(); return response()->json([ 'status' => 'success', 'message' => 'Approved by L2 Checker. Status is now Ready for Odoo push.', 'data' => $execution ]); } /** * Reject by L1 or L2 Checker. */ public function reject($id, Request $request) { $execution = OdooExecution::find($id); if (!$execution) { return response()->json(['status' => 'error', 'message' => 'Execution record not found'], 404); } $user = Auth::user(); $userId = $user ? $user->id : 0; $userName = $user ? ($user->name ?? $user->email) : 'Checker'; $remarks = $request->get('remarks', 'Rejected'); if ($execution->l1_status === 'pending') { if (!$user || !$user->isOdooCheckerL1()) { return response()->json([ 'status' => 'error', 'message' => 'Unauthorized: Only an assigned Odoo L1 Checker can reject at L1 stage.' ], 403); } if ($execution->maker_id == $userId) { return response()->json([ 'status' => 'error', 'message' => 'Maker cannot reject their own initiated entry.' ], 403); } $execution->l1_status = 'rejected'; $execution->l1_user_id = $userId; $execution->l1_user_name = $userName; $execution->l1_approved_at = Carbon::now(); $execution->l1_remarks = $remarks; } elseif ($execution->l2_status === 'pending') { if (!$user || !$user->isOdooCheckerL2()) { return response()->json([ 'status' => 'error', 'message' => 'Unauthorized: Only an assigned Odoo L2 Checker can reject at L2 stage.' ], 403); } if ($execution->maker_id == $userId) { return response()->json([ 'status' => 'error', 'message' => 'Maker cannot reject their own initiated entry.' ], 403); } $execution->l2_status = 'rejected'; $execution->l2_user_id = $userId; $execution->l2_user_name = $userName; $execution->l2_approved_at = Carbon::now(); $execution->l2_remarks = $remarks; } else { return response()->json(['status' => 'error', 'message' => 'Cannot reject: Entry is already finalized.'], 400); } $execution->odoo_status = 'rejected'; $execution->save(); return response()->json([ 'status' => 'success', 'message' => 'Entry rejected.', 'data' => $execution ]); } /** * Push entry to Odoo when Ready. */ public function pushToOdoo($id) { try { $execution = OdooExecution::find($id); if (!$execution) { return response()->json(['status' => 'error', 'message' => 'Execution record not found'], 404); } $user = Auth::user(); if (!$user || (!$user->isOdooCheckerL2() && !$user->isOdooCheckerL1())) { return response()->json([ 'status' => 'error', 'message' => 'Unauthorized: Only an assigned Odoo Checker can trigger push to Odoo.' ], 403); } // Validate Maker-Checker approvals: Both L1 and L2 must be approved if ($execution->l1_status !== 'approved' || $execution->l2_status !== 'approved') { return response()->json([ 'status' => 'error', 'message' => 'Entry is not ready for Odoo push. Both L1 and L2 approvals are required.' ], 400); } // Prevent re-pushing already pushed entries if ($execution->odoo_status === 'pushed') { return response()->json([ 'status' => 'error', 'message' => 'This entry has already been successfully pushed to Odoo (Voucher: #' . ($execution->odoo_voucher_number ?: 'N/A') . ').' ], 400); } // Prevent pushing rejected entries if ($execution->odoo_status === 'rejected') { return response()->json([ 'status' => 'error', 'message' => 'This entry was rejected and cannot be pushed to Odoo.' ], 400); } // Allow initial push ('ready') and retry push ('failed') if (!in_array($execution->odoo_status, ['ready', 'failed'])) { return response()->json([ 'status' => 'error', 'message' => 'Entry is not in a valid state for Odoo push (current status: ' . ucfirst($execution->odoo_status) . ').' ], 400); } $entries = $execution->entries; if (empty($entries)) { return response()->json(['status' => 'error', 'message' => 'No JV lines found in execution record.'], 400); } $lines = []; foreach ($entries as $e) { $lines[] = [ 'sequence' => $e['sequence'] ?? 1, 'gl_code' => $e['gl_code'] ?? '', 'reference' => $e['reference'] ?? ('REF' . $execution->application_id), 'partner_name' => $e['partner_name'] ?? $execution->customer_name, 'partner_unique_code' => $e['partner_unique_code'] ?? '', 'debit' => (float)($e['debit'] ?? 0.0), 'credit' => (float)($e['credit'] ?? 0.0), ]; } $txNo = $execution->transaction_number ?: ("TXN_" . date('YmdHis') . "_" . $execution->step_key . "_" . $execution->application_id); $postingDate = Carbon::now()->format('Y-m-d'); $payload = [ "transaction_number" => $txNo, "posting_date" => $postingDate, //"company_name" => "BLACKSOIL CAPITAL PRIVATE LIMITED", "company_code" => 110100, "type" => "payment", "lines" => $lines ]; $odooService = new OdooService(); $apiResult = $odooService->createJvEntry($payload); $statusCode = $apiResult['http_code']; $responseData = is_array($apiResult['response']) ? $apiResult['response'] : json_decode($apiResult['response'], true); $body = is_string($apiResult['response']) ? $apiResult['response'] : json_encode($apiResult['response']); if ($statusCode >= 200 && $statusCode < 300) { $odooMoveId = $responseData['odoo_move_id'] ?? null; $odooVoucherNumber = $responseData['odoo_voucher_number'] ?? null; $execution->odoo_status = 'pushed'; $execution->odoo_move_id = $odooMoveId; $execution->odoo_voucher_number = $odooVoucherNumber; $execution->odoo_response = $body; $execution->pushed_at = Carbon::now(); $execution->save(); // Also record in legacy log tables $user = Auth::user(); $createdBy = $user ? $user->id : 107; $logId = DB::table('odoo_jv_logs')->insertGetId([ 'application_id' => $execution->application_id, 'step_key' => $execution->step_key, 'month' => $execution->month, 'year' => $execution->year, 'amount' => $execution->amount, 'status' => 'success', 'transaction_number' => $txNo, 'odoo_move_id' => $odooMoveId, 'odoo_voucher_number' => $odooVoucherNumber, 'created_by' => $createdBy, 'odoo_response' => $body, 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]); $respLines = $responseData['lines'] ?? []; $respLinesBySeq = []; foreach ($respLines as $rLine) { if (isset($rLine['sequence'])) { $respLinesBySeq[$rLine['sequence']] = $rLine; } } foreach ($entries as $e) { $seq = $e['sequence'] ?? null; $moveLineId = null; if ($seq && isset($respLinesBySeq[$seq])) { $moveLineId = $respLinesBySeq[$seq]['move_line_id'] ?? null; } DB::table('odoo_jv_line_logs')->insert([ 'odoo_jv_log_id' => $logId, 'sequence' => $seq ?? 0, 'move_line_id' => $moveLineId ?? 0, 'gl_code' => $e['gl_code'] ?? null, 'account' => $e['account'] ?? null, 'debit' => (float)($e['debit'] ?? 0.0), 'credit' => (float)($e['credit'] ?? 0.0), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]); } return response()->json([ 'status' => 'success', 'message' => 'Successfully pushed JV to Odoo! Voucher Number: ' . ($odooVoucherNumber ?: 'N/A'), 'data' => $responseData ]); } else { $execution->odoo_status = 'failed'; $execution->odoo_response = $body; $execution->save(); $errorMsg = 'Failed to push JV to Odoo'; if (is_array($responseData) && isset($responseData['message'])) { $errorMsg = $responseData['message']; } return response()->json([ 'status' => 'error', 'message' => $errorMsg, 'error' => $responseData ?? $body ], 400); } } catch (\Exception $e) { Log::error('Odoo pushToOdoo Exception: ' . $e->getMessage(), [ 'trace' => $e->getTraceAsString() ]); if (isset($execution) && $execution) { try { $execution->odoo_status = 'failed'; $execution->odoo_response = json_encode([ 'status' => 'error', 'message' => $e->getMessage() ]); $execution->save(); } catch (\Exception $ignored) {} } return response()->json([ 'status' => 'error', 'message' => 'An internal error occurred: ' . $e->getMessage() ], 500); } } }