| @@ -18,18 +18,29 @@ import org.springframework.web.reactive.function.client.WebClient | |||
| import reactor.core.publisher.Mono | |||
| import kotlin.reflect.full.memberProperties | |||
| import org.springframework.context.annotation.Lazy | |||
| import org.springframework.http.HttpMethod | |||
| import org.springframework.http.HttpRequest | |||
| import org.springframework.http.HttpStatusCode | |||
| import org.springframework.web.reactive.function.BodyInserters | |||
| import org.springframework.web.reactive.function.client.ClientRequest | |||
| import org.springframework.web.reactive.function.client.ExchangeFilterFunction | |||
| import org.springframework.web.reactive.function.client.WebClientResponseException | |||
| import org.springframework.web.service.invoker.HttpRequestValues | |||
| import reactor.util.retry.Retry | |||
| import java.time.Duration | |||
| import java.util.concurrent.atomic.AtomicReference | |||
| @Service | |||
| open class ApiCallerService( | |||
| val m18Config: M18Config, | |||
| ) { | |||
| val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) | |||
| val logger: Logger = LoggerFactory.getLogger(ApiCallerService::class.java) | |||
| private val MAX_IN_MEMORY_SIZE = 10 * 1024 * 1024 | |||
| val DEFAULT_RETRY_ATTEMPTS = 5L | |||
| val webClient: WebClient = WebClient.builder() | |||
| .baseUrl(m18Config.BASE_URL) | |||
| // .baseUrl(m18Config.BASE_URL_UAT) | |||
| .defaultHeaders { headers -> | |||
| headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) | |||
| // headers.set(HttpHeaders.AUTHORIZATION, "Bearer ${m18Config.ACCESS_TOKEN}") | |||
| @@ -42,15 +53,22 @@ open class ApiCallerService( | |||
| .build() | |||
| next.exchange(updatedRequest) | |||
| } | |||
| .codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024) } | |||
| .filter(ExchangeFilterFunction.ofRequestProcessor { request -> | |||
| // logger.info("Request: ${request.method()} ${request.url()}") | |||
| Mono.just(request) | |||
| }.andThen(ExchangeFilterFunction.ofResponseProcessor { response -> | |||
| // logger.info("Response: ${response.statusCode()}") | |||
| Mono.just(response) | |||
| })) | |||
| .codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(MAX_IN_MEMORY_SIZE) } | |||
| .build() | |||
| @PostConstruct | |||
| fun init() { | |||
| updateToken() | |||
| updateToken().subscribe() | |||
| } | |||
| fun updateToken() { | |||
| fun updateToken() : Mono<String>{ | |||
| val params = M18TokenRequest( | |||
| grant_type = m18Config.GRANT_TYPE, | |||
| client_id = m18Config.CLIENT_ID, | |||
| @@ -59,96 +77,69 @@ open class ApiCallerService( | |||
| password = m18Config.PASSWORD | |||
| ) | |||
| get<M18TokenResponse, M18TokenRequest>("/oauth/token", params, null) | |||
| .subscribe( | |||
| { response -> | |||
| return get<M18TokenResponse, M18TokenRequest>("/oauth/token", params, null) | |||
| .doOnSuccess { response -> | |||
| if (m18Config.ACCESS_TOKEN != response.access_token) { | |||
| m18Config.ACCESS_TOKEN = response.access_token | |||
| println("WebClient Response stored: $response") | |||
| }, | |||
| { error -> println("WebClient Error: ${error.message}") } | |||
| ) | |||
| logger.info("Token updated: ${response.access_token}") | |||
| } | |||
| } | |||
| .doOnError { error -> logger.error("Token update failed: ${error.message}") } | |||
| .map { it.access_token } | |||
| } | |||
| // ------------------------------------ GET ------------------------------------ // | |||
| /** | |||
| * Performs a GET HTTP request to the specified URL path. | |||
| * Performs a GET HTTP request to the specified URL path with parameters. | |||
| * T: Response, U: Request | |||
| * | |||
| * @param urlPath The path to send the GET request to (after /jsf/rfws) | |||
| * @param params Optional query parameters to include in the request | |||
| * @param customHeaders Optional custom headers to include in the request. The default header includes CONTENT_TYPE, AUTHORIZATION, client_id | |||
| * @param params Optional query parameters (supports Map, MultiValueMap, or custom object) | |||
| * @param customHeaders Optional custom headers to include in the request | |||
| * @return A Mono that emits the response body converted to type T | |||
| */ | |||
| inline fun <reified T : Any> get( | |||
| inline fun <reified T : Any, reified U : Any> get( | |||
| urlPath: String, | |||
| params: MultiValueMap<String, String>, | |||
| customHeaders: Map<String, String>? | |||
| params: U? = null, | |||
| customHeaders: Map<String, String>? = null | |||
| ): Mono<T> { | |||
| println("ACCESS TOKEN: ${m18Config.ACCESS_TOKEN}") | |||
| return webClient.get() | |||
| .uri { uriBuilder -> | |||
| uriBuilder | |||
| .apply { | |||
| path(urlPath) | |||
| // convert to multiValueMap | |||
| queryParams(params) | |||
| val queryParams = when (params) { | |||
| is Map<*, *> -> LinkedMultiValueMap<String, String>().apply { | |||
| params.forEach { (k, v) -> | |||
| when (v) { | |||
| is Collection<*> -> addAll(k.toString(), v.map { it.toString() }) | |||
| else -> add(k.toString(), v.toString()) | |||
| } | |||
| .build() | |||
| } | |||
| .headers { headers -> | |||
| customHeaders?.forEach { (k, v) -> headers.set(k, v) } | |||
| } | |||
| .retrieve() | |||
| .bodyToMono(T::class.java) | |||
| // Below is for test bug (200 but error). Need to comment out bodyToMono and add jsonIgnore to data class | |||
| // .toEntity(String::class.java) // Get raw body as String | |||
| // .flatMap { entity -> | |||
| // logger.info("Response Status: ${entity.statusCode}") | |||
| // logger.info("Response Headers: ${entity.headers}") | |||
| // logger.info("Raw Response Body: ${entity.body}") | |||
| // try { | |||
| // val objectMapper = jacksonObjectMapper() | |||
| // val deserialized = objectMapper.readValue(entity.body, T::class.java) | |||
| // Mono.just(deserialized) | |||
| // } catch (e: Exception) { | |||
| // logger.error("Deserialization failed: ${e.message}") | |||
| // Mono.error(e) | |||
| // } | |||
| // } | |||
| .doOnError { error -> | |||
| println("Error occurred: ${error.message}") | |||
| } | |||
| .onErrorResume(WebClientResponseException::class.java) { error -> | |||
| logger.error("WebClientResponseException") | |||
| logger.error("Error Status: ${error.statusCode} - ${error.statusText}") | |||
| logger.error("Error Message: ${error.message}") | |||
| logger.error("Error Response: ${error.responseBodyAsString}") | |||
| if (error.statusCode == HttpStatusCode.valueOf(400)) { | |||
| updateToken() | |||
| } | |||
| Mono.error(error) | |||
| } | |||
| .onErrorResume { error -> | |||
| logger.error("Exception") | |||
| logger.error("Error Message: ${error.message}") | |||
| Mono.error(error) | |||
| null -> LinkedMultiValueMap<String, String>() | |||
| else -> LinkedMultiValueMap<String, String>().apply { | |||
| U::class.memberProperties.forEach { property -> | |||
| val key = property.name | |||
| val value = property.get(params) | |||
| when (value) { | |||
| is Collection<*> -> value.forEach { item -> add(key, item.toString()) } | |||
| else -> add(key, value.toString()) | |||
| } | |||
| } | |||
| } | |||
| .retry(5) | |||
| } | |||
| return executeGet<T>(urlPath, queryParams, customHeaders) | |||
| } | |||
| /** | |||
| * Performs a GET HTTP request to the specified URL path. | |||
| * Performs a GET HTTP request to the specified URL path without parameters. | |||
| * | |||
| * @param urlPath The path to send the GET request to (after /jsf/rfws) | |||
| * @param params Optional query parameters to include in the request | |||
| * @param customHeaders Optional custom headers to include in the request. The default header includes CONTENT_TYPE, AUTHORIZATION, client_id | |||
| * @param customHeaders Optional custom headers to include in the request | |||
| * @return A Mono that emits the response body converted to type T | |||
| */ | |||
| inline fun <reified T : Any> get( | |||
| urlPath: String, | |||
| params: Map<String, Any>?, | |||
| customHeaders: Map<String, String>? | |||
| customHeaders: Map<String, String>? = null | |||
| ): Mono<T> { | |||
| // convert to multiValueMap<String, String> | |||
| val queryParams = params?.let { paramMap -> | |||
| LinkedMultiValueMap<String, String>().apply { | |||
| @@ -161,7 +152,7 @@ open class ApiCallerService( | |||
| } | |||
| } ?: LinkedMultiValueMap<String, String>() | |||
| return get<T>(urlPath, queryParams, customHeaders) | |||
| return executeGet<T>(urlPath, queryParams, customHeaders) | |||
| } | |||
| /** | |||
| @@ -169,33 +160,87 @@ open class ApiCallerService( | |||
| * | |||
| * @param urlPath The path to send the GET request to (after /jsf/rfws) | |||
| * @param params Optional query parameters to include in the request | |||
| * @param customHeaders Optional custom headers to include in the request. The default header includes CONTENT_TYPE, AUTHORIZATION, client_id | |||
| * @return A Mono that emits the response body converted to type T | |||
| */ | |||
| inline fun <reified T : Any> get( | |||
| inline fun <reified T : Any> executeGet( | |||
| urlPath: String, | |||
| params: Map<String, Any>? | |||
| params: MultiValueMap<String, String>, | |||
| customHeaders: Map<String, String>? | |||
| ): Mono<T> { | |||
| return get<T>(urlPath, params, null) | |||
| println("ACCESS TOKEN: ${m18Config.ACCESS_TOKEN}") | |||
| return webClient.get() | |||
| .uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(params).build() } | |||
| .headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } } | |||
| .retrieve() | |||
| .bodyToMono(T::class.java) | |||
| // Below is for test bug (200 but error). Need to comment out bodyToMono and add jsonIgnore to data class | |||
| // .toEntity(String::class.java) // Get raw body as String | |||
| // .flatMap { entity -> | |||
| // logger.info("Response Status: ${entity.statusCode}") | |||
| // logger.info("Response Headers: ${entity.headers}") | |||
| // logger.info("Raw Response Body: ${entity.body}") | |||
| // try { | |||
| // val objectMapper = jacksonObjectMapper() | |||
| // val deserialized = objectMapper.readValue(entity.body, T::class.java) | |||
| // Mono.just(deserialized) | |||
| // } catch (e: Exception) { | |||
| // logger.error("Deserialization failed: ${e.message}") | |||
| // Mono.error(e) | |||
| // } | |||
| // } | |||
| .doOnError { error -> println("Error occurred: ${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.get() | |||
| .uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(params).build() } | |||
| .headers { headers -> | |||
| headers.set(HttpHeaders.AUTHORIZATION, "Bearer $newToken") | |||
| customHeaders?.forEach { (k, v) -> headers.set(k, v) } | |||
| } | |||
| .retrieve() | |||
| .bodyToMono(T::class.java) | |||
| } | |||
| } else { | |||
| Mono.error(error) | |||
| } | |||
| } | |||
| .retryWhen( | |||
| Retry.backoff(DEFAULT_RETRY_ATTEMPTS, Duration.ofSeconds(1)) | |||
| // .filter { it is WebClientResponseException && it.statusCode != HttpStatusCode.valueOf(400) } | |||
| .doBeforeRetry { signal -> logger.info("Retrying due to: ${signal.failure().message}") } | |||
| ) | |||
| } | |||
| // ------------------------------------ POST ------------------------------------ // | |||
| /** | |||
| * Performs a GET HTTP request to the specified URL path. | |||
| * Performs a POST HTTP request to the specified URL path with parameters. | |||
| * T: Response, U: Request | |||
| * | |||
| * @param urlPath The path to send the GET request to (after /jsf/rfws) | |||
| * @param params Optional query parameters to include in the request | |||
| * @param customHeaders Optional custom headers to include in the request. The default header includes CONTENT_TYPE, AUTHORIZATION, client_id | |||
| * @param urlPath The path to send the POST request to (after /jsf/rfws) | |||
| * @param params Optional query parameters (supports Map, MultiValueMap, or custom object) | |||
| * @param customHeaders Optional custom headers to include in the request | |||
| * @return A Mono that emits the response body converted to type T | |||
| */ | |||
| inline fun <reified T : Any, reified U : Any> get( | |||
| inline fun <reified T : Any, reified U : Any> post( | |||
| urlPath: String, | |||
| params: U?, | |||
| customHeaders: Map<String, String>? | |||
| params: U? = null, | |||
| customHeaders: Map<String, String>? = null | |||
| ): Mono<T> { | |||
| // convert to multiValueMap<String, String> | |||
| val queryParams = params?.let { | |||
| LinkedMultiValueMap<String, String>().apply { | |||
| val queryParams = when (params) { | |||
| is Map<*, *> -> LinkedMultiValueMap<String, String>().apply { | |||
| params.forEach { (k, v) -> | |||
| when (v) { | |||
| is Collection<*> -> addAll(k.toString(), v.map { it.toString() }) | |||
| else -> add(k.toString(), v.toString()) | |||
| } | |||
| } | |||
| } | |||
| null -> LinkedMultiValueMap<String, String>() | |||
| else -> LinkedMultiValueMap<String, String>().apply { | |||
| U::class.memberProperties.forEach { property -> | |||
| val key = property.name | |||
| val value = property.get(params) | |||
| @@ -205,25 +250,80 @@ open class ApiCallerService( | |||
| } | |||
| } | |||
| } | |||
| } | |||
| return executePost<T>(urlPath, queryParams, customHeaders) | |||
| } | |||
| /** | |||
| * Performs a POST HTTP request to the specified URL path without parameters. | |||
| * | |||
| * @param urlPath The path to send the POST request to (after /jsf/rfws) | |||
| * @param customHeaders Optional custom headers to include in the request | |||
| * @return A Mono that emits the response body converted to type T | |||
| */ | |||
| inline fun <reified T : Any> post( | |||
| urlPath: String, | |||
| params: Map<String, Any>?, | |||
| customHeaders: Map<String, String>? = null | |||
| ): Mono<T> { | |||
| // convert to multiValueMap<String, String> | |||
| val queryParams = params?.let { paramMap -> | |||
| LinkedMultiValueMap<String, String>().apply { | |||
| paramMap.forEach { (key, value) -> | |||
| when (value) { | |||
| is Collection<*> -> addAll(key, value.map { it.toString() }) | |||
| else -> add(key, value.toString()) | |||
| } | |||
| } | |||
| } | |||
| } ?: LinkedMultiValueMap<String, String>() | |||
| return get<T>(urlPath, queryParams, customHeaders) | |||
| return executePost<T>(urlPath, queryParams, customHeaders) | |||
| } | |||
| /** | |||
| * Performs a GET HTTP request to the specified URL path. | |||
| * T: Response, U: Request | |||
| * Performs a POST HTTP request to the specified URL path. | |||
| * | |||
| * @param urlPath The path to send the GET request to (after /jsf/rfws) | |||
| * @param params Optional query parameters to include in the request | |||
| * @param customHeaders Optional custom headers to include in the request. The default header includes CONTENT_TYPE, AUTHORIZATION, client_id | |||
| * @return A Mono that emits the response body converted to type T | |||
| */ | |||
| inline fun <reified T : Any, reified U : Any> get( | |||
| inline fun <reified T : Any> executePost( | |||
| urlPath: String, | |||
| params: U?, | |||
| params: MultiValueMap<String, String>, | |||
| customHeaders: Map<String, String>? | |||
| ): Mono<T> { | |||
| println("ACCESS TOKEN: ${m18Config.ACCESS_TOKEN}") | |||
| return get<T, U>(urlPath, params, null) | |||
| return webClient.post() | |||
| .uri { uriBuilder -> uriBuilder.path(urlPath).build() } | |||
| .headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } } | |||
| .body(BodyInserters.fromValue(params)) | |||
| .retrieve() | |||
| .bodyToMono(T::class.java) | |||
| .doOnError { error -> println("Error occurred: ${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.get() | |||
| .uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(params).build() } | |||
| .headers { headers -> | |||
| headers.set(HttpHeaders.AUTHORIZATION, "Bearer $newToken") | |||
| customHeaders?.forEach { (k, v) -> headers.set(k, v) } | |||
| } | |||
| .retrieve() | |||
| .bodyToMono(T::class.java) | |||
| } | |||
| } else { | |||
| Mono.error(error) | |||
| } | |||
| } | |||
| .retryWhen( | |||
| Retry.backoff(DEFAULT_RETRY_ATTEMPTS, Duration.ofSeconds(1)) | |||
| .doBeforeRetry { signal -> logger.info("Retrying due to: ${signal.failure().message}") } | |||
| ) | |||
| } | |||
| } | |||
| @@ -28,6 +28,9 @@ open class M18Config { | |||
| @Value("\${m18.config.base-url}") | |||
| lateinit var BASE_URL: String; | |||
| @Value("\${m18.config.base-url-uat}") | |||
| lateinit var BASE_URL_UAT: String; | |||
| // Supplier | |||
| // var MATERIAL_PO_SUPPLIER_NOT: List<String> = listOf("P06", "P07", "T62"); // If need oem type | |||
| @@ -1,113 +0,0 @@ | |||
| package com.ffii.fpsms.m18.service | |||
| import com.ffii.core.utils.JwtTokenUtil | |||
| import com.ffii.fpsms.m18.web.models.M18CommonRequest | |||
| import com.ffii.fpsms.modules.common.SettingNames | |||
| import com.ffii.fpsms.modules.settings.service.SettingsService | |||
| import jakarta.annotation.PostConstruct | |||
| import org.slf4j.Logger | |||
| import org.slf4j.LoggerFactory | |||
| import org.springframework.scheduling.TaskScheduler | |||
| //import org.springframework.scheduling.annotation.Async | |||
| import org.springframework.scheduling.annotation.Scheduled | |||
| import org.springframework.scheduling.support.CronTrigger | |||
| import org.springframework.stereotype.Service | |||
| import java.time.LocalDateTime | |||
| import java.time.format.DateTimeFormatter | |||
| import java.util.concurrent.ScheduledFuture | |||
| import kotlin.concurrent.Volatile | |||
| import kotlin.jvm.optionals.getOrNull | |||
| @Service | |||
| open class M18SchedulerService( | |||
| val m18PurchaseOrderService: M18PurchaseOrderService, | |||
| val m18DeliveryOrderService: M18DeliveryOrderService, | |||
| val m18MasterDataService: M18MasterDataService, | |||
| val settingsService: SettingsService, | |||
| val taskScheduler: TaskScheduler, | |||
| ) { | |||
| var logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) | |||
| val dataStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd") | |||
| val defaultCronExpression = "0 0 2 * * *"; | |||
| @Volatile | |||
| var scheduledM18Po: ScheduledFuture<*>? = null | |||
| @Volatile | |||
| var scheduledM18Master: ScheduledFuture<*>? = null | |||
| fun isValidCronExpression(cronExpression: String): Boolean { | |||
| return try { | |||
| CronTrigger(cronExpression) | |||
| true | |||
| } catch (e: IllegalArgumentException) { | |||
| false | |||
| } | |||
| } | |||
| @PostConstruct | |||
| fun init() { | |||
| scheduleM18PoTask() | |||
| scheduleM18MasterData() | |||
| } | |||
| fun commonSchedule(scheduled: ScheduledFuture<*>?, settingName: String, scheduleFunc: () -> Unit) { | |||
| scheduled?.cancel(false) | |||
| var cron = settingsService.findByName(settingName).getOrNull()?.value ?: defaultCronExpression; | |||
| if (!isValidCronExpression(cron)) { | |||
| cron = defaultCronExpression | |||
| } | |||
| scheduledM18Po = taskScheduler.schedule( | |||
| { | |||
| scheduleFunc() | |||
| }, | |||
| CronTrigger(cron) | |||
| ) | |||
| } | |||
| // Scheduler | |||
| fun scheduleM18PoTask() { | |||
| commonSchedule(scheduledM18Po, SettingNames.SCHEDULE_M18_PO, ::getM18Pos) | |||
| } | |||
| fun scheduleM18MasterData() { | |||
| commonSchedule(scheduledM18Master, SettingNames.SCHEDULE_M18_MASTER, ::getM18MasterData) | |||
| } | |||
| // Tasks | |||
| // @Async | |||
| // @Scheduled(cron = "0 0 2 * * *") // (SS/MM/HH/DD/MM/YY) | |||
| open fun getM18Pos() { | |||
| logger.info("Daily Scheduler - PO") | |||
| val currentTime = LocalDateTime.now() | |||
| val today = currentTime.toLocalDate().atStartOfDay() | |||
| val yesterday = today.minusDays(1L) | |||
| val request = M18CommonRequest( | |||
| modifiedDateTo = today.format(dataStringFormat), | |||
| modifiedDateFrom = yesterday.format(dataStringFormat) | |||
| ) | |||
| m18PurchaseOrderService.savePurchaseOrders(request); | |||
| m18DeliveryOrderService.saveDeliveryOrders(request); | |||
| logger.info("today: ${today.format(dataStringFormat)}") | |||
| logger.info("yesterday: ${yesterday.format(dataStringFormat)}") | |||
| } | |||
| open fun getM18MasterData() { | |||
| logger.info("Daily Scheduler - Master Data") | |||
| val currentTime = LocalDateTime.now() | |||
| val today = currentTime.toLocalDate().atStartOfDay() | |||
| val yesterday = today.minusDays(1L) | |||
| val request = M18CommonRequest( | |||
| modifiedDateTo = today.format(dataStringFormat), | |||
| modifiedDateFrom = yesterday.format(dataStringFormat) | |||
| ) | |||
| m18MasterDataService.saveUnits(request) | |||
| m18MasterDataService.saveProducts(request) | |||
| m18MasterDataService.saveVendors(request) | |||
| m18MasterDataService.saveBusinessUnits(request) | |||
| m18MasterDataService.saveCurrencies(request) | |||
| logger.info("today: ${today.format(dataStringFormat)}") | |||
| logger.info("yesterday: ${yesterday.format(dataStringFormat)}") | |||
| } | |||
| } | |||
| @@ -7,6 +7,7 @@ import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus | |||
| import com.ffii.fpsms.modules.master.entity.projections.SearchId | |||
| import org.springframework.data.jpa.repository.JpaRepository | |||
| import org.springframework.data.jpa.repository.Query | |||
| import org.springframework.data.repository.query.Param | |||
| import org.springframework.stereotype.Repository | |||
| import java.io.Serializable | |||
| import java.time.LocalDateTime | |||
| @@ -29,4 +30,5 @@ fun findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn( | |||
| requiredDeliveryDate: LocalDate, | |||
| status: List<DoPickOrderStatus> | |||
| ): List<DoPickOrder> | |||
| } | |||
| @@ -58,8 +58,10 @@ import org.springframework.core.io.ClassPathResource | |||
| import java.io.File | |||
| import java.io.FileNotFoundException | |||
| import com.ffii.core.support.JdbcDao; | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecord | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.ExportDNLabelsRequest | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintDNLabelsRequest | |||
| import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository | |||
| @@ -100,6 +102,8 @@ open class DeliveryOrderService( | |||
| private val inventoryLotRepository: InventoryLotRepository, | |||
| private val jdbcDao: JdbcDao, | |||
| private val pickExecutionIssueRepository: PickExecutionIssueRepository, | |||
| private val doPickOrderRepository: DoPickOrderRepository, | |||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | |||
| ) { | |||
| open fun findByM18DataLogId(m18DataLogId: Long): DeliveryOrder? { | |||
| @@ -673,63 +677,104 @@ open class DeliveryOrderService( | |||
| if (!resource.exists()) { | |||
| throw FileNotFoundException("Report file not fount: $DELIVERYNOTE_PDF") | |||
| } | |||
| val inputStream = resource.inputStream | |||
| val deliveryNote = JasperCompileManager.compileReport(inputStream) | |||
| val deliveryNoteInfo = | |||
| deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList() | |||
| val doPickOrder = doPickOrderRepository.findById(request.doPickOrderId).orElseThrow { | |||
| NoSuchElementException("DoPickOrder not found with ID: ${request.doPickOrderId}") | |||
| } | |||
| val pickOrderIds = if (doPickOrder.pickOrderId != null) { | |||
| // Single pick order case | |||
| listOf(doPickOrder.pickOrderId!!) | |||
| } else { | |||
| // Merged pick orders case - get from DoPickOrderLine | |||
| val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrder.id!!) | |||
| val orderIds = doPickOrderLines.mapNotNull { it.pickOrderId }.distinct() | |||
| val fields = mutableListOf<MutableMap<String, Any>>() | |||
| val params = mutableMapOf<String, Any>() | |||
| if (orderIds.isEmpty()) { | |||
| throw IllegalStateException("DoPickOrder ${request.doPickOrderId} has no associated pick orders") | |||
| } | |||
| orderIds | |||
| } | |||
| val deliveryOrderEntity = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.deliveryOrderIds) | |||
| val selectedTruckNo = deliveryOrderEntity?.shop?.id?.let { shopId -> | |||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | |||
| trucks.firstOrNull()?.truckLanceCode | |||
| } ?: "" | |||
| val deliveryOrderIds = if (doPickOrder.doOrderId != null) { | |||
| // Single delivery order case | |||
| listOf(doPickOrder.doOrderId!!) | |||
| } else { | |||
| // Merged delivery orders case - get from DoPickOrderLine | |||
| val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrder.id!!) | |||
| val orderIds = doPickOrderLines.mapNotNull { it.doOrderId }.distinct() | |||
| val selectedPickOrder = pickOrderRepository.findById(request.pickOrderIds).orElse(null) | |||
| if (orderIds.isEmpty()) { | |||
| throw IllegalStateException("DoPickOrder ${request.doPickOrderId} has no associated delivery orders") | |||
| } | |||
| orderIds | |||
| } | |||
| val deliveryNoteInfo = deliveryOrderIds.flatMap { deliveryOrderId -> | |||
| deliveryOrderRepository.findDeliveryOrderInfoById(deliveryOrderId) | |||
| }.toMutableList() | |||
| for (info in deliveryNoteInfo) { | |||
| val sortedLines = info.deliveryOrderLines.sortedBy { line -> | |||
| line.itemId?.let { itemId -> | |||
| getWarehouseOrderByItemId(itemId) // ✅ 改用 warehouse order | |||
| } ?: Int.MAX_VALUE | |||
| } | |||
| if (deliveryNoteInfo.isEmpty()) { | |||
| throw NoSuchElementException("Delivery orders not found for IDs: $deliveryOrderIds") | |||
| } | |||
| sortedLines.forEachIndexed { index, line -> | |||
| val pickOrderId = pickOrderIds.first() | |||
| val field = mutableMapOf<String, Any>() | |||
| val inputStream = resource.inputStream | |||
| val deliveryNote = JasperCompileManager.compileReport(inputStream) | |||
| field["sequenceNumber"] = (index + 1).toString() | |||
| field["itemNo"] = line.itemNo | |||
| field["itemName"] = line.itemName ?: "" | |||
| field["uom"] = line.uom ?: "" | |||
| field["qty"] = line.qty.toString() | |||
| field["shortName"] = line.uomShortDesc ?: "" | |||
| val fields = mutableListOf<MutableMap<String, Any>>() | |||
| val params = mutableMapOf<String, Any>() | |||
| val route = line.itemId?.let { itemId -> | |||
| getWarehouseCodeByItemId(itemId) // ✅ 使用新方法 | |||
| } ?: "" | |||
| field["route"] = route | |||
| val truckNo = doPickOrder.truckLanceCode ?: "" | |||
| val selectedPickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) | |||
| val lotNo = line.itemId?.let { itemId -> | |||
| getLotNumbersForPickOrderByItemId(itemId, request.pickOrderIds) | |||
| } ?: "" | |||
| field["lotNo"] = lotNo | |||
| val allLines = deliveryNoteInfo.flatMap { info -> | |||
| info.deliveryOrderLines.map { line -> line } | |||
| } | |||
| fields.add(field) | |||
| } | |||
| val sortedLines = allLines.sortedBy { line -> | |||
| line.itemId?.let { itemId -> | |||
| getWarehouseOrderByItemId(itemId) | |||
| } ?: Int.MAX_VALUE | |||
| } | |||
| sortedLines.forEach { line -> | |||
| val field = mutableMapOf<String, Any>() | |||
| field["sequenceNumber"] = (fields.size + 1).toString() | |||
| field["itemNo"] = line.itemNo | |||
| field["itemName"] = line.itemName ?: "" | |||
| field["uom"] = line.uom ?: "" | |||
| field["qty"] = line.qty.toString() | |||
| field["shortName"] = line.uomShortDesc ?: "" | |||
| val route = line.itemId?.let { itemId -> | |||
| getWarehouseCodeByItemId(itemId) | |||
| } ?: "" | |||
| field["route"] = route | |||
| val lotNo = line.itemId?.let { itemId -> | |||
| pickOrderIds.mapNotNull { pickOrderId -> | |||
| val lots = getLotNumbersForPickOrderByItemId(itemId, pickOrderId) | |||
| if (lots.isNotBlank()) lots else null | |||
| }.distinct().joinToString(", ") | |||
| } ?: "" | |||
| field["lotNo"] = lotNo | |||
| fields.add(field) | |||
| } | |||
| if (request.isDraft) { | |||
| params["dnTitle"] = "送貨單(初稿)" | |||
| params["colQty"] = "所需數量" | |||
| params["totalCartonTitle"] = "" | |||
| params["deliveryOrderCodeTitle"] = "" | |||
| params["deliveryOrderCode"] = "" | |||
| } else { | |||
| params["dnTitle"] = "送貨單" | |||
| params["colQty"] = "數量" | |||
| params["colQty"] = "已提數量" | |||
| params["totalCartonTitle"] = "總箱數:" | |||
| params["deliveryOrderCodeTitle"] = "送貨單編號:" | |||
| params["deliveryOrderCode"] = "GEN FROM CODE GENERATOR (NEED FIND TIMING)" | |||
| } | |||
| params["numOfCarton"] = request.numOfCarton.toString() | |||
| @@ -737,23 +782,20 @@ open class DeliveryOrderService( | |||
| params["numOfCarton"] = "" | |||
| } | |||
| params["deliveryOrderCode"] = deliveryNoteInfo[0].code | |||
| params["shopName"] = deliveryNoteInfo[0].shopName ?: "" | |||
| params["shopName"] = doPickOrder.shopName ?: deliveryNoteInfo[0].shopName ?:"" | |||
| params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: "" | |||
| params["deliveryDate"] = | |||
| deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||
| params["truckNo"] = selectedTruckNo | |||
| params["ShopPurchaseOrderNo"] = deliveryNoteInfo[0].code | |||
| params["FGPickOrderNo"] = selectedPickOrder?.code ?: "" | |||
| params["deliveryDate"] = deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||
| params["truckNo"] = truckNo | |||
| params["ShopPurchaseOrderNo"] = doPickOrder.deliveryOrderCode ?: deliveryNoteInfo.joinToString(", ") { it.code } | |||
| params["FGPickOrderNo"] = doPickOrder.pickOrderCode ?: selectedPickOrder?.code ?: "" | |||
| return mapOf( | |||
| "report" to PdfUtils.fillReport(deliveryNote, fields, params), | |||
| "filename" to deliveryNoteInfo[0].code | |||
| "filename" to deliveryNoteInfo.joinToString("_") { it.code } | |||
| ) | |||
| } | |||
| //Print Delivery Note | |||
| @Transactional | |||
| open fun printDeliveryNote(request: PrintDeliveryNoteRequest) { | |||
| @@ -761,10 +803,9 @@ open class DeliveryOrderService( | |||
| val pdf = exportDeliveryNote( | |||
| ExportDeliveryNoteRequest( | |||
| deliveryOrderIds = request.deliveryOrderId, | |||
| doPickOrderId = request.doPickOrderId, | |||
| numOfCarton = request.numOfCarton, | |||
| isDraft = request.isDraft, | |||
| pickOrderIds = request.pickOrderId | |||
| isDraft = request.isDraft | |||
| ) | |||
| ) | |||
| @@ -792,20 +833,30 @@ open class DeliveryOrderService( | |||
| if (!resource.exists()) { | |||
| throw FileNotFoundException("Label file not found: $DNLABELS_PDF") | |||
| } | |||
| val doPickOrder = doPickOrderRepository.findById(request.doPickOrderId).orElseThrow { | |||
| NoSuchElementException("DoPickOrder not found with ID: ${request.doPickOrderId}") | |||
| } | |||
| val deliveryOrderId = doPickOrder.doOrderId | |||
| ?: throw IllegalStateException("DoPickOrder has no associated delivery order") | |||
| val inputStream = resource.inputStream | |||
| val cartonLabel = JasperCompileManager.compileReport(inputStream) | |||
| val cartonLabelInfo = | |||
| deliveryOrderRepository.findDeliveryOrderInfoById(request.deliveryOrderIds).toMutableList() | |||
| deliveryOrderRepository.findDeliveryOrderInfoById(request.doPickOrderId).toMutableList() | |||
| val params = mutableMapOf<String, Any>() | |||
| val fields = mutableListOf<MutableMap<String, Any>>() | |||
| for (info in cartonLabelInfo) { | |||
| val field = mutableMapOf<String, Any>() | |||
| } | |||
| params["shopPurchaseOrderNo"] = cartonLabelInfo[0].code | |||
| params["deliveryOrderCode"] = cartonLabelInfo[0].code | |||
| params["shopPurchaseOrderNo"] = doPickOrder.deliveryOrderCode ?: cartonLabelInfo[0].code | |||
| params["deliveryOrderCode"] = "GEN FROM CODE GENERATOR (NEED FIND TIMING)" | |||
| params["shopAddress"] = cartonLabelInfo[0].shopAddress ?: "" | |||
| params["shopName"] = cartonLabelInfo[0].shopName ?: "" | |||
| params["shopName"] = doPickOrder.shopName ?: cartonLabelInfo[0].shopName ?: "" | |||
| for (cartonNumber in 1..request.numOfCarton) { | |||
| val field = mutableMapOf<String, Any>() | |||
| @@ -828,7 +879,7 @@ open class DeliveryOrderService( | |||
| val pdf = exportDNLabels( | |||
| ExportDNLabelsRequest( | |||
| deliveryOrderIds = request.deliveryOrderId, | |||
| doPickOrderId = request.doPickOrderId, | |||
| numOfCarton = request.numOfCarton | |||
| ) | |||
| ) | |||
| @@ -64,7 +64,7 @@ open class DoPickOrderService( | |||
| ) | |||
| } | |||
| fun getNextTicketNumber(datePrefix: String, storeId: String): String { | |||
| open fun getNextTicketNumber(datePrefix: String, storeId: String): String { | |||
| println("🔍 DEBUG: Getting next ticket number for date prefix: $datePrefix, store: $storeId") | |||
| try { | |||
| val sanitizedStoreId = storeId.replace("/", "") | |||
| @@ -92,18 +92,18 @@ open class DoPickOrderService( | |||
| } | |||
| } | |||
| fun save(record: DoPickOrder): DoPickOrder { | |||
| open fun save(record: DoPickOrder): DoPickOrder { | |||
| return doPickOrderRepository.save(record) | |||
| } | |||
| fun findByStoreIdOrderByTruckDepartureTime(storeId: String): List<DoPickOrder> { | |||
| open fun findByStoreIdOrderByTruckDepartureTime(storeId: String): List<DoPickOrder> { | |||
| return doPickOrderRepository.findByStoreIdAndTicketStatusOrderByTruckDepartureTimeAsc( | |||
| storeId, DoPickOrderStatus.pending | |||
| ) | |||
| } | |||
| // Add these missing methods | |||
| fun assignByStore(request: AssignByStoreRequest): MessageResponse { | |||
| open fun assignByStore(request: AssignByStoreRequest): MessageResponse { | |||
| // TODO: Implement store-based assignment logic | |||
| return MessageResponse( | |||
| id = null, | |||
| @@ -116,7 +116,7 @@ open class DoPickOrderService( | |||
| ) | |||
| } | |||
| fun releaseAssignedByStore(request: AssignByStoreRequest): MessageResponse { | |||
| open fun releaseAssignedByStore(request: AssignByStoreRequest): MessageResponse { | |||
| // TODO: Implement store-based release logic | |||
| return MessageResponse( | |||
| id = null, | |||
| @@ -130,7 +130,7 @@ open class DoPickOrderService( | |||
| } | |||
| // ✅ Updated method to set ticketReleaseTime when assigning order to user | |||
| fun updateHandledByForPickOrder(pickOrderId: Long, userId: Long): List<DoPickOrder> { | |||
| open fun updateHandledByForPickOrder(pickOrderId: Long, userId: Long): List<DoPickOrder> { | |||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| val user = userRepository.findById(userId).orElse(null) // ✅ 改用 orElse(null) | |||
| val handlerName = user?.name ?: "Unknown" | |||
| @@ -143,7 +143,7 @@ open class DoPickOrderService( | |||
| return doPickOrderRepository.saveAll(doPickOrders) | |||
| } | |||
| fun completeDoPickOrdersForPickOrder(pickOrderId: Long): List<DoPickOrder> { | |||
| open fun completeDoPickOrdersForPickOrder(pickOrderId: Long): List<DoPickOrder> { | |||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| doPickOrders.forEach { | |||
| it.ticketStatus = DoPickOrderStatus.completed | |||
| @@ -169,7 +169,7 @@ open class DoPickOrderService( | |||
| // ✅ New method to remove do_pick_order records when auto-assigning by store | |||
| // ✅ 修改方法:先复制记录到record表,再删除原记录 | |||
| @Transactional | |||
| fun removeDoPickOrdersForPickOrder(pickOrderId: Long): Int { | |||
| open fun removeDoPickOrdersForPickOrder(pickOrderId: Long): Int { | |||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| var deletedCount = 0 | |||
| @@ -229,12 +229,12 @@ open class DoPickOrderService( | |||
| return deletedCount | |||
| } | |||
| fun saveRecord(record: DoPickOrderRecord): DoPickOrderRecord { | |||
| open fun saveRecord(record: DoPickOrderRecord): DoPickOrderRecord { | |||
| return doPickOrderRecordRepository.save(record) | |||
| } | |||
| // ✅ Add method to update DoPickOrderRecord status | |||
| fun updateRecordHandledByForPickOrder(pickOrderId: Long, userId: Long): List<DoPickOrderRecord> { | |||
| open fun updateRecordHandledByForPickOrder(pickOrderId: Long, userId: Long): List<DoPickOrderRecord> { | |||
| val doPickOrderRecords = doPickOrderRecordRepository.findByPickOrderId(pickOrderId) | |||
| val user = userRepository.findById(userId).orElse(null) // ✅ 改用 orElse(null) | |||
| val handlerName = user?.name ?: "Unknown" | |||
| @@ -248,7 +248,7 @@ open class DoPickOrderService( | |||
| } | |||
| // ✅ Add method to complete DoPickOrderRecord | |||
| fun completeDoPickOrderRecordsForPickOrder(pickOrderId: Long): List<DoPickOrderRecord> { | |||
| open fun completeDoPickOrderRecordsForPickOrder(pickOrderId: Long): List<DoPickOrderRecord> { | |||
| val doPickOrderRecords = doPickOrderRecordRepository.findByPickOrderId(pickOrderId) | |||
| doPickOrderRecords.forEach { | |||
| it.ticketStatus = DoPickOrderStatus.completed | |||
| @@ -258,18 +258,18 @@ open class DoPickOrderService( | |||
| } | |||
| // Add method to find do_pick_order records by pick order ID | |||
| fun findByPickOrderId(pickOrderId: Long): List<DoPickOrder> { | |||
| open fun findByPickOrderId(pickOrderId: Long): List<DoPickOrder> { | |||
| return doPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| } | |||
| fun updateDoOrderIdForPickOrder(pickOrderId: Long, doOrderId: Long): List<DoPickOrder> { | |||
| open fun updateDoOrderIdForPickOrder(pickOrderId: Long, doOrderId: Long): List<DoPickOrder> { | |||
| val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) | |||
| doPickOrders.forEach { | |||
| it.doOrderId = doOrderId | |||
| } | |||
| return doPickOrderRepository.saveAll(doPickOrders) | |||
| } | |||
| fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary { | |||
| open fun getSummaryByStore(storeId: String, requiredDate: LocalDate?): StoreLaneSummary { | |||
| val targetDate = requiredDate ?: LocalDate.now() | |||
| println("🔍 DEBUG: Getting summary for store=$storeId, date=$targetDate") | |||
| @@ -372,7 +372,7 @@ open class DoPickOrderService( | |||
| } | |||
| } | |||
| // ✅ 修复:把 assignByLane 移到类里面 | |||
| fun assignByLane(request: AssignByLaneRequest): MessageResponse { | |||
| open fun assignByLane(request: AssignByLaneRequest): MessageResponse { | |||
| val user = userRepository.findById(request.userId).orElse(null) | |||
| ?: return MessageResponse( | |||
| id = null, code = "USER_NOT_FOUND", name = null, type = null, | |||
| @@ -41,6 +41,7 @@ class DoReleaseCoordinatorService( | |||
| private val poolSize = Runtime.getRuntime().availableProcessors() | |||
| private val executor = Executors.newFixedThreadPool(min(poolSize, 4)) | |||
| private val jobs = ConcurrentHashMap<String, BatchReleaseJobStatus>() | |||
| private fun updateBatchTicketNumbers() { | |||
| try { | |||
| val updateSql = """ | |||
| @@ -188,6 +189,7 @@ class DoReleaseCoordinatorService( | |||
| e.printStackTrace() | |||
| } | |||
| } | |||
| private fun getOrderedDeliveryOrderIds(ids: List<Long>): List<Long> { | |||
| try { | |||
| println("🔍 DEBUG: Getting ordered IDs for ${ids.size} orders") | |||
| @@ -492,7 +494,7 @@ class DoReleaseCoordinatorService( | |||
| "4F" -> "4/F" | |||
| else -> "2/F" | |||
| } | |||
| val doPickOrder = DoPickOrder( | |||
| storeId = storeId, | |||
| ticketNo = "TEMP-${System.currentTimeMillis()}", | |||
| @@ -1,6 +1,6 @@ | |||
| package com.ffii.fpsms.modules.deliveryOrder.web.models | |||
| data class ExportDNLabelsRequest ( | |||
| val deliveryOrderIds: Long, | |||
| val doPickOrderId: Long, | |||
| val numOfCarton: Int, | |||
| ) | |||
| @@ -2,8 +2,7 @@ package com.ffii.fpsms.modules.deliveryOrder.web.models | |||
| import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository | |||
| data class ExportDeliveryNoteRequest ( | |||
| val deliveryOrderIds: Long, | |||
| val doPickOrderId: Long, | |||
| val numOfCarton: Int, | |||
| val isDraft: Boolean, | |||
| val pickOrderIds: Long, | |||
| ) | |||
| @@ -1,8 +1,8 @@ | |||
| package com.ffii.fpsms.modules.deliveryOrder.web.models | |||
| data class PrintDNLabelsRequest ( | |||
| val deliveryOrderId: Long, | |||
| val doPickOrderId: Long, | |||
| val printerId: Long, | |||
| val printQty: Int?, | |||
| val numOfCarton: Int, | |||
| val numOfCarton: Int | |||
| ) | |||
| @@ -1,10 +1,9 @@ | |||
| package com.ffii.fpsms.modules.deliveryOrder.web.models | |||
| data class PrintDeliveryNoteRequest( | |||
| val deliveryOrderId: Long, | |||
| val doPickOrderId: Long, | |||
| val printerId: Long, | |||
| val printQty: Int?, | |||
| val numOfCarton: Int, | |||
| val isDraft: Boolean, | |||
| val pickOrderId: Long, | |||
| ) | |||
| @@ -713,6 +713,8 @@ open fun createBatchReleaseIssue( | |||
| FROM fpsmsdb.delivery_order_line dol | |||
| INNER JOIN fpsmsdb.items i ON i.id = dol.itemId | |||
| WHERE dol.deliveryOrderId = :deliveryOrderId | |||
| INNER JOIN fpsmsdb.items i ON i.id = dol.itemId | |||
| WHERE dol.doId = :deliveryOrderId | |||
| AND dol.deleted = 0 | |||
| """.trimIndent() | |||
| @@ -736,11 +738,11 @@ open fun createBatchReleaseIssue( | |||
| val issue = PickExecutionIssue( | |||
| id = null, | |||
| pickOrderId = null, // batch release 失败时可能还没有 pick order | |||
| pickOrderId = 0L, // batch release 失败时可能还没有 pick order | |||
| pickOrderCode = deliveryOrder["code"] as? String ?: "", | |||
| pickOrderCreateDate = LocalDate.now(), | |||
| pickExecutionDate = LocalDate.now(), | |||
| pickOrderLineId = null, // batch release 失败时可能还没有 pick order line | |||
| pickOrderLineId = 0L, // batch release 失败时可能还没有 pick order line | |||
| issueNo = issueNo, | |||
| joPickOrderId = null, | |||
| doPickOrderId = null, | |||
| @@ -5,6 +5,7 @@ import com.ffii.fpsms.modules.master.entity.Items | |||
| import com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo | |||
| import org.springframework.data.domain.Page | |||
| import org.springframework.data.domain.Pageable | |||
| import org.springframework.data.jpa.repository.Query | |||
| import org.springframework.stereotype.Repository | |||
| import java.io.Serializable | |||
| import java.util.Optional | |||
| @@ -13,7 +14,12 @@ import java.util.Optional | |||
| interface InventoryRepository: AbstractRepository<Inventory, Long> { | |||
| fun findInventoryInfoByDeletedIsFalse(): List<InventoryInfo> | |||
| fun findInventoryInfoByItemCodeContainsAndItemNameContainsAndItemTypeContainsAndDeletedIsFalse(code: String, name: String, type: String, pageable: Pageable): Page<InventoryInfo> | |||
| @Query("SELECT i FROM Inventory i " + | |||
| "WHERE (:code IS NULL OR i.item.code LIKE CONCAT('%', :code, '%')) " + | |||
| "AND (:name IS NULL OR i.item.name LIKE CONCAT('%', :name, '%')) " + | |||
| "AND (:type IS NULL OR :type = '' OR i.item.type = :type) " + | |||
| "AND i.deleted = false") | |||
| fun findInventoryInfoByItemCodeContainsAndItemNameContainsAndItemTypeAndDeletedIsFalse(code: String, name: String, type: String, pageable: Pageable): Page<InventoryInfo> | |||
| fun findInventoryInfoByItemIdInAndDeletedIsFalse(itemIds: List<Serializable>): List<InventoryInfo> | |||
| @@ -60,7 +60,7 @@ open class InventoryService( | |||
| open fun allInventoriesByPage(request: SearchInventoryRequest): RecordsRes<InventoryInfo>{ | |||
| val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10) | |||
| val response = inventoryRepository.findInventoryInfoByItemCodeContainsAndItemNameContainsAndItemTypeContainsAndDeletedIsFalse( | |||
| val response = inventoryRepository.findInventoryInfoByItemCodeContainsAndItemNameContainsAndItemTypeAndDeletedIsFalse( | |||
| code = request.code, | |||
| name = request.name, | |||
| type = request.type, | |||
| @@ -1,15 +1,6 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <!-- Created with Jaspersoft Studio version 6.17.0.final using JasperReports Library version 6.17.0-6d93193241dd8cc42629e188b94f9e0bc5722efd --> | |||
| <jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="DeliveryNotePDF" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" isIgnorePagination="true" uuid="36f9d415-527f-4152-b7b0-eea81fe06f73"> | |||
| <property name="com.jaspersoft.studio.unit." value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.pageWidth" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.topMargin" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.bottomMargin" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.leftMargin" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.rightMargin" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.columnWidth" value="pixel"/> | |||
| <property name="com.jaspersoft.studio.unit.columnSpacing" value="pixel"/> | |||
| <jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="DeliveryNotePDF" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="36f9d415-527f-4152-b7b0-eea81fe06f73"> | |||
| <parameter name="deliveryOrderCode" class="java.lang.String"> | |||
| <parameterDescription><![CDATA[DeliveryOrderCode]]></parameterDescription> | |||
| </parameter> | |||
| @@ -159,17 +150,7 @@ | |||
| <pageHeader> | |||
| <band height="110"> | |||
| <staticText> | |||
| <reportElement x="0" y="10" width="110" height="18" uuid="7f991bbe-caf4-43c1-b8e1-d85b1f2d3815"> | |||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="12"/> | |||
| </textElement> | |||
| <text><![CDATA[送貨單編號:]]></text> | |||
| </staticText> | |||
| <staticText> | |||
| <reportElement x="0" y="30" width="110" height="18" uuid="7bdf9657-d5d2-4fbf-a6c3-a5da5b292dc8"> | |||
| <reportElement x="0" y="10" width="110" height="18" uuid="7bdf9657-d5d2-4fbf-a6c3-a5da5b292dc8"> | |||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| @@ -179,7 +160,7 @@ | |||
| <text><![CDATA[送貨日期:]]></text> | |||
| </staticText> | |||
| <staticText> | |||
| <reportElement x="275" y="10" width="110" height="18" uuid="f53e2068-dd24-4151-bd2a-033c6cbda674"> | |||
| <reportElement x="0" y="30" width="110" height="18" uuid="f53e2068-dd24-4151-bd2a-033c6cbda674"> | |||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| @@ -193,20 +174,11 @@ | |||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||
| <font fontName="微軟正黑體" size="12"/> | |||
| </textElement> | |||
| <text><![CDATA[店鋪採購單編號:]]></text> | |||
| </staticText> | |||
| <textField> | |||
| <reportElement x="110" y="10" width="150" height="18" uuid="69d21d74-e625-41e9-b4bb-abde259d6620"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$P{deliveryOrderCode}]]></textFieldExpression> | |||
| </textField> | |||
| <textField> | |||
| <reportElement x="110" y="50" width="425" height="54" uuid="24a1331c-e50f-4a72-9a41-3e05b85f4c21"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| @@ -217,7 +189,7 @@ | |||
| <textFieldExpression><![CDATA[$P{ShopPurchaseOrderNo}]]></textFieldExpression> | |||
| </textField> | |||
| <textField> | |||
| <reportElement x="385" y="10" width="150" height="18" uuid="78f29b7d-c311-4c53-9c66-fda8752c9797"> | |||
| <reportElement x="110" y="30" width="150" height="18" uuid="78f29b7d-c311-4c53-9c66-fda8752c9797"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement verticalAlignment="Middle"> | |||
| @@ -226,7 +198,7 @@ | |||
| <textFieldExpression><![CDATA[$P{FGPickOrderNo}]]></textFieldExpression> | |||
| </textField> | |||
| <textField> | |||
| <reportElement x="110" y="30" width="150" height="18" uuid="e67b4047-642b-46f5-8ec7-785ead88b97e"> | |||
| <reportElement x="110" y="10" width="150" height="18" uuid="e67b4047-642b-46f5-8ec7-785ead88b97e"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement verticalAlignment="Middle"> | |||
| @@ -235,7 +207,7 @@ | |||
| <textFieldExpression><![CDATA[$P{deliveryDate}]]></textFieldExpression> | |||
| </textField> | |||
| <textField> | |||
| <reportElement x="385" y="30" width="150" height="18" uuid="8062f7b6-8917-432a-a02c-e5a5766c14e5"> | |||
| <reportElement x="385" y="10" width="150" height="18" uuid="8062f7b6-8917-432a-a02c-e5a5766c14e5"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement verticalAlignment="Middle"> | |||
| @@ -244,7 +216,7 @@ | |||
| <textFieldExpression><![CDATA[$P{numOfCarton}]]></textFieldExpression> | |||
| </textField> | |||
| <textField> | |||
| <reportElement x="275" y="30" width="110" height="18" uuid="25254ea4-e2b2-4ae0-975b-99c8f9390a64"> | |||
| <reportElement x="275" y="10" width="110" height="18" uuid="25254ea4-e2b2-4ae0-975b-99c8f9390a64"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement verticalAlignment="Middle"> | |||
| @@ -252,12 +224,30 @@ | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$P{totalCartonTitle}]]></textFieldExpression> | |||
| </textField> | |||
| <textField> | |||
| <reportElement x="385" y="30" width="150" height="18" uuid="69d21d74-e625-41e9-b4bb-abde259d6620"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$P{deliveryOrderCode}]]></textFieldExpression> | |||
| </textField> | |||
| <textField> | |||
| <reportElement x="275" y="30" width="110" height="18" uuid="db59b94d-5a33-4c84-98d9-30b8b86bd018"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$P{deliveryOrderCodeTitle}]]></textFieldExpression> | |||
| </textField> | |||
| </band> | |||
| </pageHeader> | |||
| <columnHeader> | |||
| <band height="33"> | |||
| <staticText> | |||
| <reportElement x="0" y="5" width="50" height="18" uuid="d0d76c93-d260-4b03-b116-6e7ba1fdbdd8"> | |||
| <reportElement x="0" y="5" width="40" height="18" uuid="d0d76c93-d260-4b03-b116-6e7ba1fdbdd8"> | |||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||
| @@ -268,7 +258,7 @@ | |||
| <text><![CDATA[序號]]></text> | |||
| </staticText> | |||
| <staticText> | |||
| <reportElement x="50" y="5" width="110" height="18" uuid="58a5c922-fd98-4997-9b17-16bdf9f78519"> | |||
| <reportElement x="40" y="5" width="110" height="18" uuid="58a5c922-fd98-4997-9b17-16bdf9f78519"> | |||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||
| @@ -279,7 +269,7 @@ | |||
| <text><![CDATA[路綫]]></text> | |||
| </staticText> | |||
| <staticText> | |||
| <reportElement x="160" y="5" width="80" height="18" uuid="65c27cc0-f806-4930-930c-6b3fd632a52f"> | |||
| <reportElement x="150" y="4" width="80" height="18" uuid="65c27cc0-f806-4930-930c-6b3fd632a52f"> | |||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| @@ -289,7 +279,7 @@ | |||
| <text><![CDATA[貨品編號]]></text> | |||
| </staticText> | |||
| <staticText> | |||
| <reportElement x="240" y="5" width="230" height="18" uuid="fa7ba1d5-003a-4c99-8a2f-4162756ee515"> | |||
| <reportElement x="230" y="5" width="240" height="18" uuid="fa7ba1d5-003a-4c99-8a2f-4162756ee515"> | |||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| @@ -325,28 +315,28 @@ | |||
| <detail> | |||
| <band height="42"> | |||
| <textField> | |||
| <reportElement x="0" y="1" width="50" height="18" uuid="ae87b739-dadf-452a-bc35-8c2da1a6a9a8"> | |||
| <reportElement x="0" y="1" width="40" height="18" uuid="ae87b739-dadf-452a-bc35-8c2da1a6a9a8"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement verticalAlignment="Top"> | |||
| <textElement verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{sequenceNumber}]]></textFieldExpression> | |||
| </textField> | |||
| <textField> | |||
| <reportElement x="50" y="1" width="110" height="18" uuid="b4bcfa6c-5d2e-4fba-815a-cc2fccd39213"> | |||
| <reportElement x="40" y="1" width="110" height="18" uuid="b4bcfa6c-5d2e-4fba-815a-cc2fccd39213"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement verticalAlignment="Top"> | |||
| <textElement verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{route}]]></textFieldExpression> | |||
| </textField> | |||
| <textField> | |||
| <reportElement x="160" y="1" width="80" height="18" uuid="3e4a71e7-d6e1-4da8-ae58-ef752c289a6d"> | |||
| <reportElement x="150" y="0" width="80" height="18" uuid="3e4a71e7-d6e1-4da8-ae58-ef752c289a6d"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement verticalAlignment="Top"> | |||
| <textElement verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{itemNo}]]></textFieldExpression> | |||
| @@ -355,16 +345,16 @@ | |||
| <reportElement x="470" y="0" width="84" height="18" uuid="e60b7a29-273a-4a9f-a443-f4977292c429"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement verticalAlignment="Top"> | |||
| <textElement verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{qty} + $F{shortName}]]></textFieldExpression> | |||
| </textField> | |||
| <textField> | |||
| <reportElement x="240" y="1" width="230" height="18" uuid="c2b4da75-fdca-4e99-8103-5769dea75841"> | |||
| <reportElement x="230" y="1" width="240" height="18" uuid="c2b4da75-fdca-4e99-8103-5769dea75841"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement verticalAlignment="Top"> | |||
| <textElement verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{itemName} + "(" + $F{uom} + ")"]]></textFieldExpression> | |||
| @@ -375,10 +365,10 @@ | |||
| </reportElement> | |||
| </line> | |||
| <textField> | |||
| <reportElement x="240" y="18" width="230" height="18" uuid="af701932-2e78-47d4-a131-b668200dc376"> | |||
| <reportElement x="230" y="18" width="240" height="18" uuid="af701932-2e78-47d4-a131-b668200dc376"> | |||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||
| </reportElement> | |||
| <textElement verticalAlignment="Top"> | |||
| <textElement verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{lotNo}]]></textFieldExpression> | |||
| @@ -35,6 +35,7 @@ m18: | |||
| username: testingMTMS | |||
| password: db25f2fc14cd2d2b1e7af307241f548fb03c312a | |||
| base-url: https://toa.m18saas.com/jsf/rfws | |||
| base-url-uat: https://toauat.m18saas.com/jsf/rfws | |||
| base-password: qwer1234 | |||
| supplier: | |||
| shop-po: P06, P07 | |||