Bläddra i källkod

update

controllQRcodescanWarehouse
CANCERYS\kw093 1 månad sedan
förälder
incheckning
728650068d
6 ändrade filer med 396 tillägg och 12 borttagningar
  1. +38
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/PickExcludeRule.kt
  2. +9
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/PickExcludeRuleRepository.kt
  3. +21
    -12
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt
  4. +274
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/PickExcludeRuleService.kt
  5. +26
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/PickExcludeRuleController.kt
  6. +28
    -0
      src/main/resources/db/changelog/changes/20261005_pick_exclude_rule/01_table.sql

+ 38
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/PickExcludeRule.kt Visa fil

@@ -0,0 +1,38 @@
package com.ffii.fpsms.modules.deliveryOrder.entity

import com.ffii.core.entity.BaseEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Table

@Entity
@Table(name = "pick_exclude_rule")
open class PickExcludeRule : BaseEntity<Long>() {
@Column(nullable = false, length = 2)
open var scope: String = ""

@Column(nullable = false, length = 30)
open var floor: String = ""

@Column(nullable = false, length = 30)
open var warehouse: String = ""

@Column(nullable = false, columnDefinition = "TEXT")
open var areas: String = ""

@Column(nullable = false, columnDefinition = "TEXT")
open var slots: String = ""

@Column(name = "print_mode", nullable = false, length = 20)
open var printMode: String = "hideList"

@Column(name = "rule_enabled", nullable = false)
open var ruleEnabled: Boolean = true

@Column(name = "card_id", nullable = false, length = 64)
open var cardId: String = ""

/** use, unused, or deleted. */
@Column(name = "record_status", nullable = false, length = 10)
open var recordStatus: String = "use"
}

+ 9
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/PickExcludeRuleRepository.kt Visa fil

@@ -0,0 +1,9 @@
package com.ffii.fpsms.modules.deliveryOrder.entity

import com.ffii.core.support.AbstractRepository
import org.springframework.stereotype.Repository

@Repository
interface PickExcludeRuleRepository : AbstractRepository<PickExcludeRule, Long> {
fun findByScopeAndDeletedIsFalseOrderByIdAsc(scope: String): List<PickExcludeRule>
}

+ 21
- 12
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt Visa fil

@@ -9,6 +9,7 @@ import java.util.Locale
@Service
open class DoFloorSupplierSettingsService(
private val settingsRepository: SettingsRepository,
private val pickExcludeRuleService: PickExcludeRuleService,
) {
companion object {
private const val SETTING_DO_FLOOR_SUPPLIERS_2F = "DO.floor.suppliers.2F"
@@ -51,17 +52,19 @@ open class DoFloorSupplierSettingsService(

/** DO pick / workbench default: lots in these warehouses are not suggested. */
open fun loadDoPickExcludeWarehouseCodes(): Set<String> =
warehouseCodesFromSettingOrDefault(
SETTING_DO_PICK_EXCLUDE_WAREHOUSES,
DEFAULT_DO_PICK_EXCLUDE_WAREHOUSES,
)
pickExcludeRuleService.expandedOrNull("DO")?.all
?: warehouseCodesFromSettingOrDefault(
SETTING_DO_PICK_EXCLUDE_WAREHOUSES,
DEFAULT_DO_PICK_EXCLUDE_WAREHOUSES,
)

/** JO workbench (and consumable user 246): lots in these warehouses are not suggested. */
open fun loadJoPickExcludeWarehouseCodes(): Set<String> =
warehouseCodesFromSettingOrDefault(
SETTING_JO_PICK_EXCLUDE_WAREHOUSES,
JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES.toList(),
)
pickExcludeRuleService.expandedOrNull("JO")?.all
?: warehouseCodesFromSettingOrDefault(
SETTING_JO_PICK_EXCLUDE_WAREHOUSES,
JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES.toList(),
)

/**
* hideList: excluded warehouses are omitted from the print list, and scanning their physical QR does not pass.
@@ -77,19 +80,25 @@ open class DoFloorSupplierSettingsService(
* Hide-QR warehouses stay on the list and can still be scanned.
* A missing hide-QR row keeps the old single print mode.
*/
open fun loadDoHideListWarehouseCodes(): Set<String> =
hideListCodes(
open fun loadDoHideListWarehouseCodes(): Set<String> {
val expanded = pickExcludeRuleService.expandedOrNull("DO")
if (expanded != null) return expanded.all.filter { it !in expanded.hideQr }.toSet()
return hideListCodes(
loadDoPickExcludeWarehouseCodes(),
SETTING_DO_PICK_EXCLUDE_HIDE_QR,
loadDoPickExcludePrintMode(),
)
}

open fun loadJoHideListWarehouseCodes(): Set<String> =
hideListCodes(
open fun loadJoHideListWarehouseCodes(): Set<String> {
val expanded = pickExcludeRuleService.expandedOrNull("JO")
if (expanded != null) return expanded.all.filter { it !in expanded.hideQr }.toSet()
return hideListCodes(
loadJoPickExcludeWarehouseCodes(),
SETTING_JO_PICK_EXCLUDE_HIDE_QR,
loadJoPickExcludePrintMode(),
)
}

open fun rejectsScanOfExcludedWarehouse(printMode: String): Boolean =
printMode != PRINT_MODE_HIDE_QR


+ 274
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/PickExcludeRuleService.kt Visa fil

@@ -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() }
}

+ 26
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/PickExcludeRuleController.kt Visa fil

@@ -0,0 +1,26 @@
package com.ffii.fpsms.modules.deliveryOrder.web

import com.ffii.fpsms.modules.deliveryOrder.service.PickExcludeRuleDto
import com.ffii.fpsms.modules.deliveryOrder.service.PickExcludeRuleService
import com.ffii.fpsms.modules.deliveryOrder.service.SavePickExcludeRulesRequest
import com.ffii.fpsms.modules.deliveryOrder.service.SavePickExcludeRulesResponse
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/settings/pick-exclude-rules")
class PickExcludeRuleController(
private val pickExcludeRuleService: PickExcludeRuleService,
) {
@GetMapping
fun list(@RequestParam scope: String): List<PickExcludeRuleDto> =
pickExcludeRuleService.visibleRules(scope)

@PostMapping
fun save(@RequestBody request: SavePickExcludeRulesRequest): SavePickExcludeRulesResponse =
pickExcludeRuleService.save(request)
}

+ 28
- 0
src/main/resources/db/changelog/changes/20261005_pick_exclude_rule/01_table.sql Visa fil

@@ -0,0 +1,28 @@
--liquibase formatted sql
--changeset fpsms:20261005_pick_exclude_rule

-- Active pick-exclude rules stay in settings when the JSON fits in varchar(1000).
-- `db` in DO/JO.pick.excludeRules means the rows here are the source.
-- record_status: use = applied, unused = disabled, deleted = removed from the screen.
-- unused + deleted are kept only up to 100 rows per scope.

CREATE TABLE IF NOT EXISTS `pick_exclude_rule` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`version` INT NOT NULL DEFAULT 0,
`created` DATETIME NOT NULL,
`createdBy` VARCHAR(255) NULL,
`modified` DATETIME NOT NULL,
`modifiedBy` VARCHAR(255) NULL,
`deleted` BIT(1) NOT NULL DEFAULT 0,
`scope` VARCHAR(2) NOT NULL,
`floor` VARCHAR(30) NOT NULL,
`warehouse` VARCHAR(30) NOT NULL,
`areas` TEXT NOT NULL,
`slots` TEXT NOT NULL,
`print_mode` VARCHAR(20) NOT NULL,
`rule_enabled` BIT(1) NOT NULL,
`card_id` VARCHAR(64) NOT NULL,
`record_status` VARCHAR(10) NOT NULL,
PRIMARY KEY (`id`),
INDEX `pick_exclude_rule_scope_idx` (`scope`, `record_status`)
);

Laddar…
Avbryt
Spara