From 8015a0f951026cd0312c8d7f7eec8f46857fe9dc Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Tue, 24 Feb 2026 13:37:39 +0800 Subject: [PATCH] fixing the auto gen Job order, adding the GRN --- .../fpsms/api/service/ApiCallerService.kt | 41 +++++++++++ .../m18/model/GoodsReceiptNoteRequest.kt | 40 +++++++++++ .../m18/model/GoodsReceiptNoteResponse.kt | 15 ++++ .../m18/service/M18GoodsReceiptNoteService.kt | 72 +++++++++++++++++++ .../jobOrder/service/JobOrderService.kt | 3 + .../service/ProductionScheduleService.kt | 26 ++++--- 6 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/m18/model/GoodsReceiptNoteRequest.kt create mode 100644 src/main/java/com/ffii/fpsms/m18/model/GoodsReceiptNoteResponse.kt create mode 100644 src/main/java/com/ffii/fpsms/m18/service/M18GoodsReceiptNoteService.kt diff --git a/src/main/java/com/ffii/fpsms/api/service/ApiCallerService.kt b/src/main/java/com/ffii/fpsms/api/service/ApiCallerService.kt index ffd3257..73565aa 100644 --- a/src/main/java/com/ffii/fpsms/api/service/ApiCallerService.kt +++ b/src/main/java/com/ffii/fpsms/api/service/ApiCallerService.kt @@ -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 put( + urlPath: String, + queryParams: MultiValueMap, + body: Any, + customHeaders: Map? = null + ): Mono { + 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) + } + } + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/m18/model/GoodsReceiptNoteRequest.kt b/src/main/java/com/ffii/fpsms/m18/model/GoodsReceiptNoteRequest.kt new file mode 100644 index 0000000..e12222b --- /dev/null +++ b/src/main/java/com/ffii/fpsms/m18/model/GoodsReceiptNoteRequest.kt @@ -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, +) + +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, +) + +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, +) diff --git a/src/main/java/com/ffii/fpsms/m18/model/GoodsReceiptNoteResponse.kt b/src/main/java/com/ffii/fpsms/m18/model/GoodsReceiptNoteResponse.kt new file mode 100644 index 0000000..f706bd3 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/m18/model/GoodsReceiptNoteResponse.kt @@ -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 = emptyList(), + val status: Boolean = false, +) + +data class GoodsReceiptNoteMessage( + val msgDetail: String? = null, + val msgCode: String? = null, +) diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18GoodsReceiptNoteService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18GoodsReceiptNoteService.kt new file mode 100644 index 0000000..0a85ce2 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/m18/service/M18GoodsReceiptNoteService.kt @@ -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 { + val queryParams = LinkedMultiValueMap().apply { + add("menuCode", MENU_CODE_AN) + param?.let { add("param", it) } + } + return apiCallerService.put( + 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}") + } + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt index 5d558b9..b5fc659 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt @@ -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"); diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt index c2f18d7..0dfad66 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt @@ -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)