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>/is', $raw, $pMatches)) {
$pText = trim(strip_tags($pMatches[1]));
$errorMessage = $title . ': ' . $pText;
} elseif (preg_match('/
(.*?)<\/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);
}
}
}