|
|
@@ -0,0 +1,274 @@ |
|
|
|
|
|
package com.ffii.fpsms.modules.deliveryOrder.service |
|
|
|
|
|
|
|
|
|
|
|
import com.fasterxml.jackson.core.type.TypeReference |
|
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper |
|
|
|
|
|
import com.ffii.fpsms.modules.deliveryOrder.entity.PickExcludeRule |
|
|
|
|
|
import com.ffii.fpsms.modules.deliveryOrder.entity.PickExcludeRuleRepository |
|
|
|
|
|
import com.ffii.fpsms.modules.master.entity.WarehouseRepository |
|
|
|
|
|
import com.ffii.fpsms.modules.settings.entity.SettingsRepository |
|
|
|
|
|
import org.springframework.stereotype.Service |
|
|
|
|
|
import org.springframework.transaction.annotation.Transactional |
|
|
|
|
|
import java.util.Locale |
|
|
|
|
|
|
|
|
|
|
|
data class PickExcludeRuleDto( |
|
|
|
|
|
val floor: String = "", |
|
|
|
|
|
val warehouse: String = "", |
|
|
|
|
|
val areas: String = "", |
|
|
|
|
|
val slots: String = "", |
|
|
|
|
|
val printMode: String = "hideList", |
|
|
|
|
|
val enabled: Boolean = true, |
|
|
|
|
|
val cardId: String = "", |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
data class SavePickExcludeRulesRequest( |
|
|
|
|
|
val scope: String = "", |
|
|
|
|
|
val rules: List<PickExcludeRuleDto> = emptyList(), |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
data class SavePickExcludeRulesResponse( |
|
|
|
|
|
val source: String, |
|
|
|
|
|
val rules: List<PickExcludeRuleDto>, |
|
|
|
|
|
val droppedUnusedOrDeleted: Int, |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
data class ExpandedExcludeCodes( |
|
|
|
|
|
val all: Set<String>, |
|
|
|
|
|
val hideQr: Set<String>, |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Pick-exclude rules are stored as JSON in `settings` when that JSON fits in varchar(1000). |
|
|
|
|
|
* The setting value `db` means [pick_exclude_rule] is the source. |
|
|
|
|
|
* Disabled rules are `unused` and removed rules are `deleted`. Those two statuses together |
|
|
|
|
|
* are kept only up to [ARCHIVE_LIMIT] rows; older deleted rows go first, then older unused rows. |
|
|
|
|
|
*/ |
|
|
|
|
|
@Service |
|
|
|
|
|
open class PickExcludeRuleService( |
|
|
|
|
|
private val settingsRepository: SettingsRepository, |
|
|
|
|
|
private val warehouseRepository: WarehouseRepository, |
|
|
|
|
|
private val pickExcludeRuleRepository: PickExcludeRuleRepository, |
|
|
|
|
|
private val objectMapper: ObjectMapper, |
|
|
|
|
|
) { |
|
|
|
|
|
companion object { |
|
|
|
|
|
const val SOURCE_DB = "db" |
|
|
|
|
|
const val ARCHIVE_LIMIT = 100 |
|
|
|
|
|
const val SETTINGS_VALUE_LIMIT = 1000 |
|
|
|
|
|
const val STATUS_USE = "use" |
|
|
|
|
|
const val STATUS_UNUSED = "unused" |
|
|
|
|
|
const val STATUS_DELETED = "deleted" |
|
|
|
|
|
private const val EMPTY = "-" |
|
|
|
|
|
private const val SCOPE_DO = "DO" |
|
|
|
|
|
private const val SCOPE_JO = "JO" |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** Null means the legacy warehouse-code setting is still the source. */ |
|
|
|
|
|
open fun expandedOrNull(scope: String): ExpandedExcludeCodes? { |
|
|
|
|
|
val normalized = normalizeScope(scope) |
|
|
|
|
|
val raw = readSetting(rulesSetting(normalized)) |
|
|
|
|
|
if (raw == SOURCE_DB) { |
|
|
|
|
|
return expand(rowsToDtos(loadRows(normalized).filter { it.recordStatus == STATUS_USE })) |
|
|
|
|
|
} |
|
|
|
|
|
if (raw.startsWith("[") || raw.startsWith("{")) { |
|
|
|
|
|
return expand(parseRules(raw).filter { it.enabled }) |
|
|
|
|
|
} |
|
|
|
|
|
return null |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
open fun visibleRules(scope: String): List<PickExcludeRuleDto> { |
|
|
|
|
|
val normalized = normalizeScope(scope) |
|
|
|
|
|
val raw = readSetting(rulesSetting(normalized)) |
|
|
|
|
|
val rows = loadRows(normalized) |
|
|
|
|
|
if (raw == SOURCE_DB) { |
|
|
|
|
|
return rowsToDtos(rows.filter { it.recordStatus != STATUS_DELETED }) |
|
|
|
|
|
} |
|
|
|
|
|
val unused = rowsToDtos(rows.filter { it.recordStatus == STATUS_UNUSED }) |
|
|
|
|
|
if (raw.startsWith("[") || raw.startsWith("{")) { |
|
|
|
|
|
val parsed = parseRules(raw) |
|
|
|
|
|
val parsedIds = parsed.map { it.cardId }.toSet() |
|
|
|
|
|
return parsed + unused.filter { it.cardId !in parsedIds } |
|
|
|
|
|
} |
|
|
|
|
|
return unused |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Transactional |
|
|
|
|
|
open fun save(request: SavePickExcludeRulesRequest): SavePickExcludeRulesResponse { |
|
|
|
|
|
val scope = normalizeScope(request.scope) |
|
|
|
|
|
val incoming = request.rules.map { normalizeRule(it) } |
|
|
|
|
|
val previousLive = visibleRules(scope) |
|
|
|
|
|
val incomingIds = incoming.map { it.cardId }.toSet() |
|
|
|
|
|
val newlyDeleted = previousLive.filter { it.cardId !in incomingIds } |
|
|
|
|
|
val existingDeleted = rowsToDtos(loadRows(scope).filter { it.recordStatus == STATUS_DELETED }) |
|
|
|
|
|
val deleted = (existingDeleted + newlyDeleted).distinctBy { it.cardId } |
|
|
|
|
|
val used = incoming.filter { it.enabled } |
|
|
|
|
|
var unused = incoming.filter { !it.enabled }.toMutableList() |
|
|
|
|
|
var deletedKept = deleted.toMutableList() |
|
|
|
|
|
var dropped = 0 |
|
|
|
|
|
while (unused.size + deletedKept.size > ARCHIVE_LIMIT) { |
|
|
|
|
|
if (deletedKept.isNotEmpty()) { |
|
|
|
|
|
deletedKept.removeAt(0) |
|
|
|
|
|
} else { |
|
|
|
|
|
unused.removeAt(0) |
|
|
|
|
|
} |
|
|
|
|
|
dropped += 1 |
|
|
|
|
|
} |
|
|
|
|
|
val kept = used + unused |
|
|
|
|
|
val activeJson = objectMapper.writeValueAsString(used) |
|
|
|
|
|
val source = if (activeJson.length <= SETTINGS_VALUE_LIMIT) "json" else SOURCE_DB |
|
|
|
|
|
replaceRows(scope, source, kept, deletedKept) |
|
|
|
|
|
writeLegacyLists(scope, expand(used)) |
|
|
|
|
|
writeSetting(rulesSetting(scope), if (source == SOURCE_DB) SOURCE_DB else activeJson) |
|
|
|
|
|
return SavePickExcludeRulesResponse(source = source, rules = kept, droppedUnusedOrDeleted = dropped) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun replaceRows( |
|
|
|
|
|
scope: String, |
|
|
|
|
|
source: String, |
|
|
|
|
|
kept: List<PickExcludeRuleDto>, |
|
|
|
|
|
deleted: List<PickExcludeRuleDto>, |
|
|
|
|
|
) { |
|
|
|
|
|
val existing = loadRows(scope) |
|
|
|
|
|
if (existing.isNotEmpty()) pickExcludeRuleRepository.deleteAll(existing) |
|
|
|
|
|
val rows = mutableListOf<Pair<PickExcludeRuleDto, String>>() |
|
|
|
|
|
if (source == SOURCE_DB) { |
|
|
|
|
|
kept.forEach { dto -> |
|
|
|
|
|
rows += dto to if (dto.enabled) STATUS_USE else STATUS_UNUSED |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
kept.filter { !it.enabled }.forEach { dto -> |
|
|
|
|
|
rows += dto to STATUS_UNUSED |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
val keptIds = kept.map { it.cardId }.toSet() |
|
|
|
|
|
deleted.forEach { dto -> |
|
|
|
|
|
if (dto.cardId !in keptIds) rows += dto to STATUS_DELETED |
|
|
|
|
|
} |
|
|
|
|
|
rows.forEach { (dto, status) -> |
|
|
|
|
|
val row = PickExcludeRule() |
|
|
|
|
|
row.scope = scope |
|
|
|
|
|
row.floor = dto.floor |
|
|
|
|
|
row.warehouse = dto.warehouse |
|
|
|
|
|
row.areas = dto.areas |
|
|
|
|
|
row.slots = dto.slots |
|
|
|
|
|
row.printMode = dto.printMode |
|
|
|
|
|
row.ruleEnabled = status == STATUS_USE |
|
|
|
|
|
row.cardId = dto.cardId |
|
|
|
|
|
row.recordStatus = status |
|
|
|
|
|
pickExcludeRuleRepository.save(row) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun expand(rules: List<PickExcludeRuleDto>): ExpandedExcludeCodes { |
|
|
|
|
|
val warehouses = warehouseRepository.findAllByDeletedIsFalse() |
|
|
|
|
|
val all = linkedSetOf<String>() |
|
|
|
|
|
val hideQr = linkedSetOf<String>() |
|
|
|
|
|
for (rule in rules) { |
|
|
|
|
|
if (!rule.enabled) continue |
|
|
|
|
|
val areas = tokens(rule.areas) |
|
|
|
|
|
val allAreas = areas.any { it == "*" } |
|
|
|
|
|
val slots = tokens(rule.slots) |
|
|
|
|
|
if (slots.any { it == "-" }) continue |
|
|
|
|
|
for (row in warehouses) { |
|
|
|
|
|
val code = row.code?.trim().orEmpty() |
|
|
|
|
|
if (code.isEmpty()) continue |
|
|
|
|
|
if (!row.store_id.orEmpty().trim().equals(rule.floor, ignoreCase = true)) continue |
|
|
|
|
|
if (!row.warehouse.orEmpty().trim().equals(rule.warehouse, ignoreCase = true)) continue |
|
|
|
|
|
val area = row.area.orEmpty().trim() |
|
|
|
|
|
if (!allAreas && areas.none { it.equals(area, ignoreCase = true) }) continue |
|
|
|
|
|
val slot = row.slot.orEmpty().trim() |
|
|
|
|
|
if (slots.isNotEmpty() && slots.none { it.equals(slot, ignoreCase = true) }) continue |
|
|
|
|
|
val upper = code.uppercase(Locale.ROOT) |
|
|
|
|
|
all.add(upper) |
|
|
|
|
|
if (rule.printMode == DoFloorSupplierSettingsService.PRINT_MODE_HIDE_QR) hideQr.add(upper) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return ExpandedExcludeCodes(all, hideQr) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun writeLegacyLists(scope: String, expanded: ExpandedExcludeCodes) { |
|
|
|
|
|
val csv = expanded.all.joinToString(",") |
|
|
|
|
|
val hideQr = expanded.hideQr.joinToString(",") |
|
|
|
|
|
val warehouseSetting = if (scope == SCOPE_DO) { |
|
|
|
|
|
DoFloorSupplierSettingsService.SETTING_DO_PICK_EXCLUDE_WAREHOUSES |
|
|
|
|
|
} else { |
|
|
|
|
|
DoFloorSupplierSettingsService.SETTING_JO_PICK_EXCLUDE_WAREHOUSES |
|
|
|
|
|
} |
|
|
|
|
|
val hideQrSetting = if (scope == SCOPE_DO) { |
|
|
|
|
|
DoFloorSupplierSettingsService.SETTING_DO_PICK_EXCLUDE_HIDE_QR |
|
|
|
|
|
} else { |
|
|
|
|
|
DoFloorSupplierSettingsService.SETTING_JO_PICK_EXCLUDE_HIDE_QR |
|
|
|
|
|
} |
|
|
|
|
|
val printModeSetting = if (scope == SCOPE_DO) { |
|
|
|
|
|
DoFloorSupplierSettingsService.SETTING_DO_PICK_EXCLUDE_PRINT_MODE |
|
|
|
|
|
} else { |
|
|
|
|
|
DoFloorSupplierSettingsService.SETTING_JO_PICK_EXCLUDE_PRINT_MODE |
|
|
|
|
|
} |
|
|
|
|
|
writeSetting(warehouseSetting, if (csv.length <= SETTINGS_VALUE_LIMIT) csv.ifEmpty { EMPTY } else EMPTY) |
|
|
|
|
|
writeSetting(hideQrSetting, if (hideQr.length <= SETTINGS_VALUE_LIMIT) hideQr.ifEmpty { EMPTY } else EMPTY) |
|
|
|
|
|
val mode = if (expanded.all.isNotEmpty() && expanded.hideQr.size == expanded.all.size) { |
|
|
|
|
|
DoFloorSupplierSettingsService.PRINT_MODE_HIDE_QR |
|
|
|
|
|
} else { |
|
|
|
|
|
DoFloorSupplierSettingsService.PRINT_MODE_HIDE_LIST |
|
|
|
|
|
} |
|
|
|
|
|
writeSetting(printModeSetting, mode) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun loadRows(scope: String): List<PickExcludeRule> = |
|
|
|
|
|
pickExcludeRuleRepository.findByScopeAndDeletedIsFalseOrderByIdAsc(scope) |
|
|
|
|
|
|
|
|
|
|
|
private fun rowsToDtos(rows: List<PickExcludeRule>): List<PickExcludeRuleDto> = |
|
|
|
|
|
rows.map { row -> |
|
|
|
|
|
PickExcludeRuleDto( |
|
|
|
|
|
floor = row.floor, |
|
|
|
|
|
warehouse = row.warehouse, |
|
|
|
|
|
areas = row.areas, |
|
|
|
|
|
slots = row.slots, |
|
|
|
|
|
printMode = row.printMode, |
|
|
|
|
|
enabled = row.recordStatus == STATUS_USE, |
|
|
|
|
|
cardId = row.cardId, |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun parseRules(raw: String): List<PickExcludeRuleDto> = |
|
|
|
|
|
try { |
|
|
|
|
|
objectMapper.readValue(raw, object : TypeReference<List<PickExcludeRuleDto>>() {}).map { normalizeRule(it) } |
|
|
|
|
|
} catch (_: Exception) { |
|
|
|
|
|
emptyList() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun normalizeRule(rule: PickExcludeRuleDto): PickExcludeRuleDto { |
|
|
|
|
|
val cardId = rule.cardId.trim().ifEmpty { "c${System.nanoTime().toString(36)}" } |
|
|
|
|
|
val mode = if (rule.printMode == DoFloorSupplierSettingsService.PRINT_MODE_HIDE_QR) { |
|
|
|
|
|
DoFloorSupplierSettingsService.PRINT_MODE_HIDE_QR |
|
|
|
|
|
} else { |
|
|
|
|
|
DoFloorSupplierSettingsService.PRINT_MODE_HIDE_LIST |
|
|
|
|
|
} |
|
|
|
|
|
return rule.copy( |
|
|
|
|
|
floor = rule.floor.trim(), |
|
|
|
|
|
warehouse = rule.warehouse.trim(), |
|
|
|
|
|
areas = rule.areas.trim(), |
|
|
|
|
|
slots = rule.slots.trim(), |
|
|
|
|
|
printMode = mode, |
|
|
|
|
|
cardId = cardId, |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun normalizeScope(scope: String): String { |
|
|
|
|
|
val value = scope.trim().uppercase(Locale.ROOT) |
|
|
|
|
|
return if (value == SCOPE_JO) SCOPE_JO else SCOPE_DO |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun rulesSetting(scope: String): String = |
|
|
|
|
|
if (scope == SCOPE_JO) "JO.pick.excludeRules" else "DO.pick.excludeRules" |
|
|
|
|
|
|
|
|
|
|
|
private fun readSetting(name: String): String = |
|
|
|
|
|
settingsRepository.findByName(name).map { it.value }.orElse(null)?.trim().orEmpty() |
|
|
|
|
|
|
|
|
|
|
|
private fun writeSetting(name: String, value: String) { |
|
|
|
|
|
val row = settingsRepository.findByName(name).orElse(null) ?: return |
|
|
|
|
|
row.value = value.ifEmpty { EMPTY } |
|
|
|
|
|
settingsRepository.save(row) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private fun tokens(raw: String): List<String> = |
|
|
|
|
|
raw.split(",").map { it.trim() }.filter { it.isNotEmpty() } |
|
|
|
|
|
} |