Przeglądaj źródła

added a report to check bom sync history

production
[email protected] 1 tydzień temu
rodzic
commit
193a1824a2
3 zmienionych plików z 214 dodań i 0 usunięć
  1. +27
    -0
      src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLogRepository.kt
  2. +165
    -0
      src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt
  3. +22
    -0
      src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt

+ 27
- 0
src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLogRepository.kt Wyświetl plik

@@ -1,6 +1,8 @@
package com.ffii.fpsms.m18.entity package com.ffii.fpsms.m18.entity


import com.ffii.core.support.AbstractRepository import com.ffii.core.support.AbstractRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param


interface M18BomShopSyncLogRepository : AbstractRepository<M18BomShopSyncLog, Long> { interface M18BomShopSyncLogRepository : AbstractRepository<M18BomShopSyncLog, Long> {
fun findFirstByBomIdOrderByIdDesc(bomId: Long): M18BomShopSyncLog? fun findFirstByBomIdOrderByIdDesc(bomId: Long): M18BomShopSyncLog?
@@ -19,4 +21,29 @@ interface M18BomShopSyncLogRepository : AbstractRepository<M18BomShopSyncLog, Lo
bomId: Long, bomId: Long,
m18HeaderCode: String, m18HeaderCode: String,
): M18BomShopSyncLog? ): M18BomShopSyncLog?

@Query(
"""
SELECT l FROM M18BomShopSyncLog l
WHERE l.deleted = false
AND (:syncDateStart IS NULL OR l.created >= :syncDateStart)
AND (:syncDateEnd IS NULL OR l.created <= :syncDateEnd)
AND (
:finishedItemCode IS NULL OR :finishedItemCode = ''
OR LOWER(l.finishedItemCode) LIKE LOWER(CONCAT('%', :finishedItemCode, '%'))
)
AND (
:syncStatus IS NULL OR :syncStatus = '' OR :syncStatus = 'all'
OR (:syncStatus = 'success' AND l.synced = true)
OR (:syncStatus = 'failed' AND l.synced = false)
)
ORDER BY l.created DESC, l.id DESC
""",
)
fun searchForReport(
@Param("syncDateStart") syncDateStart: java.time.LocalDateTime?,
@Param("syncDateEnd") syncDateEnd: java.time.LocalDateTime?,
@Param("finishedItemCode") finishedItemCode: String?,
@Param("syncStatus") syncStatus: String?,
): List<M18BomShopSyncLog>
} }

+ 165
- 0
src/main/java/com/ffii/fpsms/modules/report/service/M18BomShopSyncReportService.kt Wyświetl plik

@@ -0,0 +1,165 @@
package com.ffii.fpsms.modules.report.service

import com.fasterxml.jackson.databind.ObjectMapper
import com.ffii.fpsms.m18.entity.M18BomShopSyncLog
import com.ffii.fpsms.m18.entity.M18BomShopSyncLogRepository
import com.ffii.fpsms.m18.model.GoodsReceiptNoteResponse
import com.ffii.fpsms.m18.model.M18BomForShopSaveRequest
import com.ffii.fpsms.m18.model.M18UdfProductSaveValue
import com.ffii.fpsms.modules.master.entity.BomRepository
import com.ffii.fpsms.modules.master.entity.ItemsRepository
import org.springframework.stereotype.Service
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

@Service
open class M18BomShopSyncReportService(
private val m18BomShopSyncLogRepository: M18BomShopSyncLogRepository,
private val bomRepository: BomRepository,
private val itemsRepository: ItemsRepository,
private val objectMapper: ObjectMapper,
) {
private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
private val versionFromHeaderCode = Regex("V(\\d+)$", RegexOption.IGNORE_CASE)

open fun searchBomShopSyncHistoryReport(
syncDateStart: String?,
syncDateEnd: String?,
finishedItemCode: String?,
syncStatus: String?,
): Map<String, Any> {
val start = parseDateStart(syncDateStart)
val end = parseDateEnd(syncDateEnd)
val itemFilter = finishedItemCode?.trim()?.takeIf { it.isNotEmpty() }
val statusFilter = syncStatus?.trim()?.lowercase()?.takeIf { it.isNotEmpty() && it != "all" }

val logs = m18BomShopSyncLogRepository.searchForReport(
syncDateStart = start,
syncDateEnd = end,
finishedItemCode = itemFilter,
syncStatus = statusFilter,
)

val syncRows = mutableListOf<Map<String, Any?>>()
val materialRows = mutableListOf<Map<String, Any?>>()
var successCount = 0
var failedCount = 0
var skippedUnchangedCount = 0

for (log in logs) {
val bomId = log.bomId ?: continue
val bom = bomRepository.findByIdAndDeletedIsFalse(bomId)
val finishedCode = log.finishedItemCode?.trim().orEmpty()
val itemName =
finishedCode.takeIf { it.isNotEmpty() }
?.let { itemsRepository.findByCodeAndDeletedFalse(it)?.name?.trim() }
?: bom?.name?.trim()?.takeIf { it.isNotEmpty() }
val headerCode = log.m18HeaderCode?.trim().orEmpty()
val version = extractVersion(headerCode)
val status = resolveSyncStatus(log)
when (status) {
"SUCCESS" -> successCount++
"SKIPPED_UNCHANGED" -> skippedUnchangedCount++
else -> failedCount++
}
val syncAt = log.created?.format(dateTimeFormatter)
val failureReason = resolveFailureReason(log).takeIf { status == "FAILED" }

syncRows.add(
linkedMapOf(
"syncLogId" to log.id,
"syncDateTime" to syncAt,
"bomId" to bomId,
"bomRoutingCode" to bom?.code,
"finishedItemCode" to finishedCode.ifEmpty { null },
"finishedItemName" to itemName,
"m18HeaderCode" to headerCode.ifEmpty { null },
"version" to version,
"m18RecordId" to log.m18RecordId?.takeIf { it > 0L },
"syncStatus" to status,
"synced" to log.synced,
"m18ApiStatus" to log.m18ApiStatus,
"failureReason" to failureReason,
"message" to log.message,
),
)

val materials = parseMaterialLines(log)
for (line in materials) {
materialRows.add(
linkedMapOf(
"syncLogId" to log.id,
"syncDateTime" to syncAt,
"bomId" to bomId,
"finishedItemCode" to finishedCode.ifEmpty { null },
"m18HeaderCode" to headerCode.ifEmpty { null },
"version" to version,
"syncStatus" to status,
"lineNo" to line.itemNo?.trim(),
"materialName" to line.udfIngredients?.trim(),
"udfProductM18Id" to line.udfProduct,
"udfBaseUnit" to line.udfBaseUnit,
"udfQty" to line.udfqty,
"udfSupplierM18Id" to line.udfSupplier,
"udfPurchaseUnitM18Id" to line.udfpurchaseUnit,
),
)
}
}

return mapOf(
"summary" to mapOf(
"totalAttempts" to logs.size,
"success" to successCount,
"skippedUnchanged" to skippedUnchangedCount,
"failed" to failedCount,
"syncDateStart" to syncDateStart,
"syncDateEnd" to syncDateEnd,
),
"syncRows" to syncRows,
"materialRows" to materialRows,
)
}

private fun parseDateStart(raw: String?): LocalDateTime? {
if (raw.isNullOrBlank()) return null
return LocalDate.parse(raw.trim().replace("/", "-")).atStartOfDay()
}

private fun parseDateEnd(raw: String?): LocalDateTime? {
if (raw.isNullOrBlank()) return null
return LocalDate.parse(raw.trim().replace("/", "-")).atTime(23, 59, 59, 999_999_999)
}

private fun extractVersion(headerCode: String?): String? {
if (headerCode.isNullOrBlank()) return null
return versionFromHeaderCode.find(headerCode.trim())?.groupValues?.get(1)
}

private fun resolveSyncStatus(log: M18BomShopSyncLog): String = when {
!log.synced -> "FAILED"
log.message?.contains("skippedUnchanged", ignoreCase = true) == true -> "SKIPPED_UNCHANGED"
else -> "SUCCESS"
}

private fun resolveFailureReason(log: M18BomShopSyncLog): String? {
log.message?.trim()?.takeIf { it.isNotEmpty() }?.let { return it }
val json = log.responseJson?.trim().orEmpty().ifEmpty { return null }
return runCatching {
val resp = objectMapper.readValue(json, GoodsReceiptNoteResponse::class.java)
resp.messages
.joinToString("; ") { m -> m.msgDetail?.trim().orEmpty().ifEmpty { m.msgCode.orEmpty() } }
.trim()
.ifEmpty { null }
}.getOrNull()
}

private fun parseMaterialLines(log: M18BomShopSyncLog): List<M18UdfProductSaveValue> {
val json = log.requestJson?.trim().orEmpty().ifEmpty { return emptyList() }
return runCatching {
objectMapper.readValue(json, M18BomForShopSaveRequest::class.java)
.udfproduct.values
}.getOrElse { emptyList() }
}
}

+ 22
- 0
src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt Wyświetl plik

@@ -22,12 +22,14 @@ import java.time.LocalDate
import java.time.LocalTime import java.time.LocalTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import com.ffii.fpsms.modules.common.SecurityUtils import com.ffii.fpsms.modules.common.SecurityUtils
import com.ffii.fpsms.modules.report.service.M18BomShopSyncReportService
import com.ffii.fpsms.modules.report.service.ReportService import com.ffii.fpsms.modules.report.service.ReportService


@RestController @RestController
@RequestMapping("/report") @RequestMapping("/report")
class ReportController( class ReportController(
private val reportService: ReportService, private val reportService: ReportService,
private val m18BomShopSyncReportService: M18BomShopSyncReportService,
) { ) {
private data class ExcelStyles( private data class ExcelStyles(
val title: XSSFCellStyle, val title: XSSFCellStyle,
@@ -979,6 +981,26 @@ class ReportController(
) )
} }


/**
* M18 BOM Shop sync history (from [com.ffii.fpsms.m18.entity.M18BomShopSyncLog]).
* JSON for Excel export on the report page: sync attempts + material lines per version.
*
* Example: `/report/bom-shop-sync-history?syncDateStart=2026-06-01&syncDateEnd=2026-06-13`
*/
@GetMapping("/bom-shop-sync-history")
fun getBomShopSyncHistoryReport(
@RequestParam(required = false) syncDateStart: String?,
@RequestParam(required = false) syncDateEnd: String?,
@RequestParam(required = false) finishedItemCode: String?,
@RequestParam(required = false) syncStatus: String?,
): Map<String, Any> =
m18BomShopSyncReportService.searchBomShopSyncHistoryReport(
syncDateStart = syncDateStart,
syncDateEnd = syncDateEnd,
finishedItemCode = finishedItemCode,
syncStatus = syncStatus,
)

companion object { companion object {
/** GRN report fields only users with ADMIN authority may see */ /** GRN report fields only users with ADMIN authority may see */
private val GRN_FINANCIAL_KEYS = setOf("unitPrice", "lineAmount", "currencyCode") private val GRN_FINANCIAL_KEYS = setOf("unitPrice", "lineAmount", "currencyCode")


Ładowanie…
Anuluj
Zapisz