| @@ -326,4 +326,45 @@ open class ApiCallerService( | |||
| .doBeforeRetry { signal -> logger.info("Retrying due to: ${signal.failure().message}") } | |||
| ) | |||
| } | |||
| // ------------------------------------ PUT ------------------------------------ // | |||
| /** | |||
| * Performs a PUT HTTP request to the specified URL path with query parameters and JSON body. | |||
| * | |||
| * @param urlPath The path to send the PUT request to (e.g. /root/api/save/an) | |||
| * @param queryParams Query parameters (e.g. menuCode=an, param optional) | |||
| * @param body The request body object (serialized as JSON) | |||
| * @param customHeaders Optional custom headers | |||
| * @return A Mono that emits the response body converted to type T | |||
| */ | |||
| inline fun <reified T : Any> put( | |||
| urlPath: String, | |||
| queryParams: MultiValueMap<String, String>, | |||
| body: Any, | |||
| customHeaders: Map<String, String>? = null | |||
| ): Mono<T> { | |||
| return webClient.put() | |||
| .uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(queryParams).build() } | |||
| .headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } } | |||
| .bodyValue(body) | |||
| .retrieve() | |||
| .bodyToMono(T::class.java) | |||
| .doOnError { error -> logger.error("PUT error: ${error.message}") } | |||
| .onErrorResume(WebClientResponseException::class.java) { error -> | |||
| logger.error("WebClientResponseException: ${error.statusCode} - ${error.statusText}, Body: ${error.responseBodyAsString}") | |||
| if (error.statusCode == HttpStatusCode.valueOf(400) || error.statusCode == HttpStatusCode.valueOf(401)) { | |||
| updateToken().flatMap { newToken -> | |||
| webClient.put() | |||
| .uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(queryParams).build() } | |||
| .header(HttpHeaders.AUTHORIZATION, "Bearer $newToken") | |||
| .headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } } | |||
| .bodyValue(body) | |||
| .retrieve() | |||
| .bodyToMono(T::class.java) | |||
| } | |||
| } else { | |||
| Mono.error(error) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,40 @@ | |||
| package com.ffii.fpsms.m18.model | |||
| /** | |||
| * Request body for M18 Goods Receipt Note (AN) save API. | |||
| * PUT /root/api/save/an?menuCode=an | |||
| */ | |||
| data class GoodsReceiptNoteRequest( | |||
| val mainan: GoodsReceiptNoteMainan, | |||
| val ant: GoodsReceiptNoteAnt, | |||
| ) | |||
| data class GoodsReceiptNoteMainan( | |||
| val values: List<GoodsReceiptNoteMainanValue>, | |||
| ) | |||
| data class GoodsReceiptNoteMainanValue( | |||
| val beId: Int, | |||
| val code: String, | |||
| val venId: Int, | |||
| val curId: Int, | |||
| val rate: Number, | |||
| val flowTypeId: Int, | |||
| val staffId: Int, | |||
| ) | |||
| data class GoodsReceiptNoteAnt( | |||
| val values: List<GoodsReceiptNoteAntValue>, | |||
| ) | |||
| data class GoodsReceiptNoteAntValue( | |||
| val sourceType: String, | |||
| val sourceId: Long, | |||
| val sourceLot: String, | |||
| val proId: Int, | |||
| val locId: Int, | |||
| val unitId: Int, | |||
| val qty: Number, | |||
| val up: Number, | |||
| val amt: Number, | |||
| ) | |||
| @@ -0,0 +1,15 @@ | |||
| package com.ffii.fpsms.m18.model | |||
| /** | |||
| * Response from M18 Goods Receipt Note (AN) save API. | |||
| */ | |||
| data class GoodsReceiptNoteResponse( | |||
| val recordId: Long = 0, | |||
| val messages: List<GoodsReceiptNoteMessage> = emptyList(), | |||
| val status: Boolean = false, | |||
| ) | |||
| data class GoodsReceiptNoteMessage( | |||
| val msgDetail: String? = null, | |||
| val msgCode: String? = null, | |||
| ) | |||
| @@ -0,0 +1,72 @@ | |||
| package com.ffii.fpsms.m18.service | |||
| import com.ffii.fpsms.api.service.ApiCallerService | |||
| import com.ffii.fpsms.m18.M18Config | |||
| import com.ffii.fpsms.m18.model.GoodsReceiptNoteRequest | |||
| import com.ffii.fpsms.m18.model.GoodsReceiptNoteResponse | |||
| import org.slf4j.Logger | |||
| import org.slf4j.LoggerFactory | |||
| import org.springframework.stereotype.Service | |||
| import org.springframework.util.LinkedMultiValueMap | |||
| import reactor.core.publisher.Mono | |||
| /** | |||
| * Service to create Goods Receipt Note (AN) in M18 via the save API. | |||
| * API: PUT http://[server]/jsf/rfws/root/api/save/an | |||
| * Query: menuCode=an (required), param (optional, JSON string) | |||
| * Headers: authorization (Bearer token), client_id | |||
| */ | |||
| @Service | |||
| open class M18GoodsReceiptNoteService( | |||
| private val m18Config: M18Config, | |||
| private val apiCallerService: ApiCallerService, | |||
| ) { | |||
| private val logger: Logger = LoggerFactory.getLogger(M18GoodsReceiptNoteService::class.java) | |||
| private val M18_SAVE_GOODS_RECEIPT_NOTE_API = "/root/api/save/an" | |||
| private val MENU_CODE_AN = "an" | |||
| /** | |||
| * Creates a goods receipt note in M18. | |||
| * | |||
| * @param request The request body containing mainan (header) and ant (lines). | |||
| * @param param Optional extra parameters in JSON format (query param "param"). | |||
| * @return The M18 response with recordId, messages, and status; or null on failure. | |||
| */ | |||
| open fun createGoodsReceiptNote( | |||
| request: GoodsReceiptNoteRequest, | |||
| param: String? = null, | |||
| ): GoodsReceiptNoteResponse? { | |||
| return createGoodsReceiptNoteMono(request, param).block() | |||
| } | |||
| /** | |||
| * Creates a goods receipt note in M18 (reactive). | |||
| * | |||
| * @param request The request body containing mainan (header) and ant (lines). | |||
| * @param param Optional extra parameters in JSON format (query param "param"). | |||
| * @return Mono of the M18 response. | |||
| */ | |||
| open fun createGoodsReceiptNoteMono( | |||
| request: GoodsReceiptNoteRequest, | |||
| param: String? = null, | |||
| ): Mono<GoodsReceiptNoteResponse> { | |||
| val queryParams = LinkedMultiValueMap<String, String>().apply { | |||
| add("menuCode", MENU_CODE_AN) | |||
| param?.let { add("param", it) } | |||
| } | |||
| return apiCallerService.put<GoodsReceiptNoteResponse>( | |||
| urlPath = M18_SAVE_GOODS_RECEIPT_NOTE_API, | |||
| queryParams = queryParams, | |||
| body = request, | |||
| ).doOnSuccess { response -> | |||
| if (response.status) { | |||
| logger.info("Goods receipt note created in M18. recordId=${response.recordId}") | |||
| } else { | |||
| logger.warn("M18 save AN returned status=false. recordId=${response.recordId}, messages=${response.messages}") | |||
| } | |||
| }.doOnError { e -> | |||
| logger.error("Failed to create goods receipt note in M18: ${e.message}") | |||
| } | |||
| } | |||
| } | |||
| @@ -351,6 +351,9 @@ open class JobOrderService( | |||
| ) | |||
| } | |||
| open fun jobOrderDetailByProdScheduleLineId(prodScheduleLineId: Long): JobOrderDetail = | |||
| jobOrderDetailByPsId(prodScheduleLineId) | |||
| open fun jobOrderDetailByItemId(itemId: Long): JobOrderDetail { | |||
| val sqlResult = jobOrderRepository.findJobOrderByItemId(itemId) ?: throw NoSuchElementException("Job Order not found with itemId: $itemId"); | |||
| @@ -420,14 +420,14 @@ open class ProductionScheduleService( | |||
| val itemId = item.id | |||
| ?: throw IllegalStateException("Item ID is missing for Production Schedule Line $prodScheduleLineId.") | |||
| try { | |||
| jobOrderService.jobOrderDetailByItemId(itemId) | |||
| logger.info("jobOrderDetailByItemId ok itemId:$itemId") | |||
| } catch (e: NoSuchElementException) { | |||
| //try { | |||
| // jobOrderService.jobOrderDetailByItemId(itemId) | |||
| // logger.info("jobOrderDetailByItemId ok itemId:$itemId") | |||
| //} catch (e: NoSuchElementException) { | |||
| //only do with no JO is working | |||
| logger.info("NoSuchElementException itemId:$itemId") | |||
| //logger.info("NoSuchElementException itemId:$itemId") | |||
| try { | |||
| jobOrderService.jobOrderDetailByItemId(itemId) | |||
| jobOrderService.jobOrderDetailByProdScheduleLineId(prodScheduleLineId) | |||
| } catch (e: NoSuchElementException) { | |||
| val bom = bomService.findByItemId(itemId) | |||
| ?: throw NoSuchElementException("BOM not found for Item ID $itemId.") | |||
| @@ -449,11 +449,15 @@ open class ProductionScheduleService( | |||
| logger.info("[releaseProdSchedule] prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder) | |||
| //repeat(prodScheduleLine.needNoOfJobOrder) { | |||
| // 6. Create Job Order | |||
| val produceAt = prodScheduleLine.productionSchedule?.produceAt | |||
| val joRequest = CreateJobOrderRequest( | |||
| bomId = bom.id, // bom is guaranteed non-null here | |||
| reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())), | |||
| approverId = approver?.id, | |||
| planStart = produceAt, | |||
| planEnd = produceAt, | |||
| // CRUCIAL FIX: Use the line ID, not the parent schedule ID | |||
| prodScheduleLineId = prodScheduleLine.id!! | |||
| ) | |||
| @@ -473,7 +477,7 @@ open class ProductionScheduleService( | |||
| //} | |||
| } | |||
| } | |||
| //} | |||
| // No need to fetch latest detail inside the loop | |||
| } | |||
| @@ -542,13 +546,17 @@ open class ProductionScheduleService( | |||
| val approver = SecurityUtils.getUser().getOrNull() // Get approver once | |||
| val produceAt = prodScheduleLine.productionSchedule?.produceAt | |||
| val joRequest = CreateJobOrderRequest( | |||
| bomId = bom.id, // bom is guaranteed non-null here | |||
| reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())), | |||
| approverId = approver?.id, | |||
| planStart = produceAt, | |||
| planEnd = produceAt, | |||
| // CRUCIAL FIX: Use the line ID, not the parent schedule ID | |||
| prodScheduleLineId = prodScheduleLine.id!! | |||
| prodScheduleLineId = prodScheduleLine.id!! | |||
| ) | |||
| // Assuming createJobOrder returns the created Job Order (jo) | |||