| @@ -326,4 +326,45 @@ open class ApiCallerService( | |||||
| .doBeforeRetry { signal -> logger.info("Retrying due to: ${signal.failure().message}") } | .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 { | open fun jobOrderDetailByItemId(itemId: Long): JobOrderDetail { | ||||
| val sqlResult = jobOrderRepository.findJobOrderByItemId(itemId) ?: throw NoSuchElementException("Job Order not found with itemId: $itemId"); | 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 | val itemId = item.id | ||||
| ?: throw IllegalStateException("Item ID is missing for Production Schedule Line $prodScheduleLineId.") | ?: 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 | //only do with no JO is working | ||||
| logger.info("NoSuchElementException itemId:$itemId") | |||||
| //logger.info("NoSuchElementException itemId:$itemId") | |||||
| try { | try { | ||||
| jobOrderService.jobOrderDetailByItemId(itemId) | |||||
| jobOrderService.jobOrderDetailByProdScheduleLineId(prodScheduleLineId) | |||||
| } catch (e: NoSuchElementException) { | } catch (e: NoSuchElementException) { | ||||
| val bom = bomService.findByItemId(itemId) | val bom = bomService.findByItemId(itemId) | ||||
| ?: throw NoSuchElementException("BOM not found for Item ID $itemId.") | ?: throw NoSuchElementException("BOM not found for Item ID $itemId.") | ||||
| @@ -449,11 +449,15 @@ open class ProductionScheduleService( | |||||
| logger.info("[releaseProdSchedule] prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder) | logger.info("[releaseProdSchedule] prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder) | ||||
| //repeat(prodScheduleLine.needNoOfJobOrder) { | //repeat(prodScheduleLine.needNoOfJobOrder) { | ||||
| // 6. Create Job Order | // 6. Create Job Order | ||||
| val produceAt = prodScheduleLine.productionSchedule?.produceAt | |||||
| val joRequest = CreateJobOrderRequest( | val joRequest = CreateJobOrderRequest( | ||||
| bomId = bom.id, // bom is guaranteed non-null here | bomId = bom.id, // bom is guaranteed non-null here | ||||
| reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())), | reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())), | ||||
| approverId = approver?.id, | approverId = approver?.id, | ||||
| planStart = produceAt, | |||||
| planEnd = produceAt, | |||||
| // CRUCIAL FIX: Use the line ID, not the parent schedule ID | // CRUCIAL FIX: Use the line ID, not the parent schedule ID | ||||
| prodScheduleLineId = prodScheduleLine.id!! | prodScheduleLineId = prodScheduleLine.id!! | ||||
| ) | ) | ||||
| @@ -473,7 +477,7 @@ open class ProductionScheduleService( | |||||
| //} | //} | ||||
| } | } | ||||
| } | |||||
| //} | |||||
| // No need to fetch latest detail inside the loop | // 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 approver = SecurityUtils.getUser().getOrNull() // Get approver once | ||||
| val produceAt = prodScheduleLine.productionSchedule?.produceAt | |||||
| val joRequest = CreateJobOrderRequest( | val joRequest = CreateJobOrderRequest( | ||||
| bomId = bom.id, // bom is guaranteed non-null here | bomId = bom.id, // bom is guaranteed non-null here | ||||
| reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())), | reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())), | ||||
| approverId = approver?.id, | approverId = approver?.id, | ||||
| planStart = produceAt, | |||||
| planEnd = produceAt, | |||||
| // CRUCIAL FIX: Use the line ID, not the parent schedule ID | // 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) | // Assuming createJobOrder returns the created Job Order (jo) | ||||