Procházet zdrojové kódy

new supplier

isEtra
new do chart
do saerch batch release button put down
not lot requied qty show 0 fix
production
CANCERYS\kw093 před 1 měsícem
rodič
revize
870fbca20e
25 změnil soubory, kde provedl 552 přidání a 196 odebrání
  1. +6
    -6
      src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt
  2. +3
    -3
      src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt
  3. +29
    -17
      src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt
  4. +7
    -3
      src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt
  5. +2
    -2
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrder.kt
  6. +4
    -4
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrderRepository.kt
  7. +3
    -3
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt
  8. +52
    -82
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  9. +95
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt
  10. +7
    -20
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt
  11. +11
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchDopoAssignmentService.kt
  12. +205
    -11
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt
  13. +20
    -9
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt
  14. +3
    -3
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt
  15. +17
    -2
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoWorkbenchController.kt
  16. +16
    -3
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt
  17. +5
    -4
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt
  18. +1
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/SaveDeliveryOrderRequest.kt
  19. +3
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt
  20. +7
    -15
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt
  21. +14
    -2
      src/main/java/com/ffii/fpsms/modules/settings/web/SettingsController.java
  22. +10
    -6
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt
  23. +8
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt
  24. +18
    -0
      src/main/resources/db/changelog/changes/20260514_Enson/01_setting.sql
  25. +6
    -0
      src/main/resources/db/changelog/changes/20260514_Enson/02_setting.sql

+ 6
- 6
src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt Zobrazit soubor

@@ -154,20 +154,20 @@ open class M18DeliveryOrderService(

open fun saveDeliveryOrders(request: M18CommonRequest): SyncResult {
val deliveryOrdersWithType = getDeliveryOrdersWithType(request)
return saveDeliveryOrdersWithPreparedList(deliveryOrdersWithType, syncIsEtra = false)
return saveDeliveryOrdersWithPreparedList(deliveryOrdersWithType, syncisExtra = false)
}

/**
* Sync a single M18 shop PO / delivery order by document [code], same search pattern as
* [com.ffii.fpsms.m18.service.M18PurchaseOrderService.savePurchaseOrderByCode].
*
* @param isEtraSync when true, persist local `delivery_order.isEtra=true` (manual DO(加單) sync).
* @param isExtraSync when true, persist local `delivery_order.isExtra=true` (manual DO(加單) sync).
* No M18-side "加單" filtering is used.
* @param newOnly when true, skip if a non-deleted local DO already exists with the same `code`.
*/
open fun saveDeliveryOrderByCode(
code: String,
isEtraSync: Boolean = false,
isExtraSync: Boolean = false,
newOnly: Boolean = false,
): SyncResult {
if (newOnly && deliveryOrderRepository.existsByCodeAndDeletedIsFalse(code)) {
@@ -210,12 +210,12 @@ open class M18DeliveryOrderService(
query = conds
)

return saveDeliveryOrdersWithPreparedList(prepared, syncIsEtra = isEtraSync)
return saveDeliveryOrdersWithPreparedList(prepared, syncisExtra = isExtraSync)
}

private fun saveDeliveryOrdersWithPreparedList(
deliveryOrdersWithType: M18PurchaseOrderListResponseWithType?,
syncIsEtra: Boolean = false,
syncisExtra: Boolean = false,
): SyncResult {
logger.info("--------------------------------------------Start - Saving M18 Delivery Order--------------------------------------------")

@@ -303,7 +303,7 @@ open class M18DeliveryOrderService(
handlerId = null,
m18BeId = mainpo.beId,
deleted = mainpo.udfIsVoid == true,
isEtra = syncIsEtra,
isExtra = syncisExtra,
)

val saveDeliveryOrderResponse =


+ 3
- 3
src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt Zobrazit soubor

@@ -82,14 +82,14 @@ class M18TestController (

@GetMapping("/test/do-by-code")
fun testSyncDoByCode(@RequestParam code: String): SyncResult {
return m18DeliveryOrderService.saveDeliveryOrderByCode(code, isEtraSync = false)
return m18DeliveryOrderService.saveDeliveryOrderByCode(code, isExtraSync = false)
}

/** DO(加單):手動按 code 同步,並寫入本地 [DeliveryOrder.isEtra]=true(不做 M18 端加單條件過濾) */
/** DO(加單):手動按 code 同步,並寫入本地 [DeliveryOrder.isExtra]=true(不做 M18 端加單條件過濾) */
@GetMapping("/test/do-by-code-extra")
fun testSyncDoByCodeExtra(@RequestParam code: String): SyncResult {
// 加單 tab: only sync when it's a NEW order (not existing in local system)
return m18DeliveryOrderService.saveDeliveryOrderByCode(code, isEtraSync = true, newOnly = true)
return m18DeliveryOrderService.saveDeliveryOrderByCode(code, isExtraSync = true, newOnly = true)
}

@GetMapping("/test/product-by-code")


+ 29
- 17
src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt Zobrazit soubor

@@ -721,23 +721,27 @@ open class ChartService(

/**
* Staff delivery performance: daily pick ticket count and total time per staff.
* Uses do_pick_order_record (handler = handledBy); time = sum of (ticketCompleteDateTime - ticketReleaseTime) per record.
* Optionally use do_pick_order_line_record for line count; here orderCount = number of completed pick tickets.
* Uses delivery_order_pick_order (handler = handledBy); time = sum of
* (ticketCompleteDateTime - ticketReleaseTime) per completed ticket.
* staffNos: when non-empty, filter to these staff by user.staffNo (multi-select).
* storeIdNull: when true, only rows with dop.storeId IS NULL (takes precedence over storeId).
* storeId: when non-blank and storeIdNull is not true, filter dop.storeId equality (trimmed).
*/
fun getStaffDeliveryPerformance(
startDate: LocalDate?,
endDate: LocalDate?,
staffNos: List<String>?
staffNos: List<String>?,
storeId: String?,
storeIdNull: Boolean?,
): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND DATE(dpor.ticketCompleteDateTime) >= :startDate"
"AND DATE(dop.ticketCompleteDateTime) >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND DATE(dpor.ticketCompleteDateTime) <= :endDate"
"AND DATE(dop.ticketCompleteDateTime) <= :endDate"
} else ""
val staffSql = if (!staffNos.isNullOrEmpty()) {
val nos = staffNos.map { it.trim() }.filter { it.isNotBlank() }
@@ -746,25 +750,33 @@ open class ChartService(
"AND u.staffNo IN (:staffNos)"
}
} else ""
val storeSql = when {
storeIdNull == true -> "AND dop.storeId IS NULL"
!storeId.isNullOrBlank() -> {
args["filterStoreId"] = storeId.trim()
"AND dop.storeId = :filterStoreId"
}
else -> ""
}
val sql = """
SELECT
DATE_FORMAT(dpor.ticketCompleteDateTime, '%Y-%m-%d') AS date,
COALESCE(u.name, dpor.handler_name, 'Unknown') AS staffName,
COUNT(dpor.id) AS orderCount,
DATE_FORMAT(dop.ticketCompleteDateTime, '%Y-%m-%d') AS date,
COALESCE(NULLIF(TRIM(COALESCE(u.name, '')), ''), dop.handlerName, 'Unknown') AS staffName,
COUNT(dop.id) AS orderCount,
COALESCE(SUM(
CASE
WHEN dpor.ticket_release_time IS NOT NULL AND dpor.ticketCompleteDateTime IS NOT NULL
THEN GREATEST(0, TIMESTAMPDIFF(MINUTE, dpor.ticket_release_time, dpor.ticketCompleteDateTime))
WHEN dop.ticketReleaseTime IS NOT NULL AND dop.ticketCompleteDateTime IS NOT NULL
THEN GREATEST(0, TIMESTAMPDIFF(MINUTE, dop.ticketReleaseTime, dop.ticketCompleteDateTime))
ELSE 0
END
), 0) AS totalMinutes
FROM do_pick_order_record dpor
LEFT JOIN user u ON dpor.handled_by = u.id AND u.deleted = 0
WHERE dpor.deleted = 0
AND dpor.ticket_status = 'completed'
AND dpor.ticketCompleteDateTime IS NOT NULL
$startSql $endSql $staffSql
GROUP BY DATE(dpor.ticketCompleteDateTime), dpor.handled_by, u.name, dpor.handler_name
FROM delivery_order_pick_order dop
LEFT JOIN user u ON dop.handledBy = u.id AND u.deleted = 0
WHERE dop.deleted = 0
AND LOWER(COALESCE(dop.ticketStatus, '')) = 'completed'
AND dop.ticketCompleteDateTime IS NOT NULL
$startSql $endSql $staffSql $storeSql
GROUP BY DATE(dop.ticketCompleteDateTime), dop.handledBy, u.name, dop.handlerName
ORDER BY date, orderCount DESC
""".trimIndent()
return jdbcDao.queryForList(sql, args)


+ 7
- 3
src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt Zobrazit soubor

@@ -192,16 +192,20 @@ class ChartController(
chartService.getStaffDeliveryPerformanceHandlers()

/**
* GET /chart/staff-delivery-performance?startDate=&endDate=&staffNo=A001&staffNo=A002
* Returns [{ date, staffName, orderCount, totalMinutes }]. Data from do_pick_order_record (handled_by), orderCount = completed pick tickets, totalMinutes = sum(ticketCompleteDateTime - ticketReleaseTime).
* GET /chart/staff-delivery-performance?startDate=&endDate=&staffNo=A001&staffNo=A002&storeId=2/F&storeIdNull=true
* Returns [{ date, staffName, orderCount, totalMinutes }]. Data from delivery_order_pick_order
* (handledBy), orderCount = completed pick tickets, totalMinutes = sum(ticketCompleteDateTime - ticketReleaseTime).
* Optional storeId filters delivery_order_pick_order.storeId; storeIdNull=true means IS NULL (overrides storeId).
*/
@GetMapping("/staff-delivery-performance")
fun getStaffDeliveryPerformance(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
@RequestParam(required = false) staffNo: List<String>?,
@RequestParam(required = false) storeId: String?,
@RequestParam(required = false) storeIdNull: Boolean?,
): List<Map<String, Any>> =
chartService.getStaffDeliveryPerformance(startDate, endDate, staffNo)
chartService.getStaffDeliveryPerformance(startDate, endDate, staffNo, storeId, storeIdNull)

// ---------- Job order reports ----------



+ 2
- 2
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrder.kt Zobrazit soubor

@@ -64,6 +64,6 @@ open class DeliveryOrder: BaseEntity<Long>() {
open var m18BeId: Long? = null

/** 加單:由 M18「加單」專用同步標記;一般 DO 為 false */
@Column(name = "isEtra", nullable = false)
open var isEtra: Boolean = false
@Column(name = "isExtra", nullable = false)
open var isExtra: Boolean = false
}

+ 4
- 4
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrderRepository.kt Zobrazit soubor

@@ -111,7 +111,7 @@ fun searchDoLite(
and (:status is null or d.status = :status)
and (:etaStart is null or d.estimatedArrivalDate >= :etaStart)
and (:etaEnd is null or d.estimatedArrivalDate < :etaEnd)
and (:isEtra is null or d.isEtra = :isEtra)
and (:isExtra is null or d.isExtra = :isExtra)
order by d.id desc
""")
fun searchDoLitePage(
@@ -120,7 +120,7 @@ fun searchDoLitePage(
@Param("status") status: DeliveryOrderStatus?,
@Param("etaStart") etaStart: LocalDateTime?,
@Param("etaEnd") etaEnd: LocalDateTime?,
@Param("isEtra") isEtra: Boolean?,
@Param("isExtra") isExtra: Boolean?,
pageable: Pageable
): Page<DeliveryOrderInfoLite>

@@ -136,7 +136,7 @@ fun searchDoLitePage(
and (:status is null or d.status = :status)
and (:etaStart is null or d.estimatedArrivalDate >= :etaStart)
and (:etaEnd is null or d.estimatedArrivalDate < :etaEnd)
and (:isEtra is null or d.isEtra = :isEtra)
and (:isExtra is null or d.isExtra = :isExtra)
and d.supplier is not null
and d.supplier.code in :allowedSupplierCodes
order by d.id desc
@@ -148,7 +148,7 @@ fun searchDoLitePageWithSupplierCodes(
@Param("status") status: DeliveryOrderStatus?,
@Param("etaStart") etaStart: LocalDateTime?,
@Param("etaEnd") etaEnd: LocalDateTime?,
@Param("isEtra") isEtra: Boolean?,
@Param("isExtra") isExtra: Boolean?,
@Param("allowedSupplierCodes") allowedSupplierCodes: List<String>,
pageable: Pageable,
): Page<DeliveryOrderInfoLite>


+ 3
- 3
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt Zobrazit soubor

@@ -48,8 +48,8 @@ interface DeliveryOrderInfoLite {
@get:Value("#{target.shop?.addr3}")
val shopAddress: String?

@get:Value("#{target.isEtra}")
val isEtra: Boolean
@get:Value("#{target.isExtra}")
val isExtra: Boolean
}
data class DeliveryOrderInfoLiteDto(
val id: Long,
@@ -61,5 +61,5 @@ data class DeliveryOrderInfoLiteDto(
val supplierName: String?,
val shopAddress: String?,
val truckLanceCode: String?,
val isEtra: Boolean = false,
val isExtra: Boolean = false,
)

+ 52
- 82
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt Zobrazit soubor

@@ -90,7 +90,6 @@ import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo
import java.util.Locale
import org.slf4j.Logger

@Service
open class DeliveryOrderService(
private val deliveryOrderRepository: DeliveryOrderRepository,
@@ -121,23 +120,23 @@ open class DeliveryOrderService(
private val doPickOrderLineRepository: DoPickOrderLineRepository,
private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository,
private val itemsRepository: ItemsRepository,
private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService,
) {
/**
* 樓層篩選:2F → P07/P06D(車線- 族);4F → P06B(P06B_ 族);全部 → 三者
* 車線-X 仍屬該 DO 的 supplier,故 P06B+車線-X 不會出現在 2F,P06D+車線-X 不會出現在 4F
* 樓層篩選:2F/4F/ALL 由 [DoFloorSupplierSettingsService] 讀 `settings`
* 車線-X 仍依 DO supplier 所屬樓層出現在對應 tab
*/
private fun allowedSupplierCodesForFloor(floor: String?): List<String> {
val f = floor?.trim()?.uppercase(Locale.ROOT).orEmpty()
if (f.isEmpty() || f == "ALL" || f == "All") {
return listOf("P06B", "P07", "P06D")
}
return when (f) {
"2F" -> listOf("P07", "P06D")
"4F" -> listOf("P06B")
else -> listOf("P06B", "P07", "P06D")
}
}
private fun allowedSupplierCodesForFloor(floor: String?): List<String> =
doFloorSupplierSettingsService.allowedSupplierCodesForFloor(floor)

private fun loadDoFloorSupplierLists(): Pair<List<String>, List<String>> =
doFloorSupplierSettingsService.loadDoFloorSupplierLists()

private fun preferredStoreFloorForSupplier(
supplierCode: String?,
suppliers2F: List<String>,
suppliers4F: List<String>,
): String = doFloorSupplierSettingsService.preferredStoreFloorForSupplier(supplierCode, suppliers2F, suppliers4F)
open fun searchDoLiteByPage(
code: String?,
shopName: String?,
@@ -147,7 +146,7 @@ open class DeliveryOrderService(
pageSize: Int?,
truckLanceCode: String?,
floor: String? = null,
isEtra: Boolean? = null,
isExtra: Boolean? = null,
): RecordsRes<DeliveryOrderInfoLiteDto> {

val page = (pageNum ?: 1) - 1
@@ -169,7 +168,7 @@ open class DeliveryOrderService(
status = statusEnum,
etaStart = etaStart,
etaEnd = etaEnd,
isEtra = isEtra,
isExtra = isExtra,
allowedSupplierCodes = allowedForFloor,
pageable = PageRequest.of(0, 100_000),
)
@@ -181,6 +180,7 @@ open class DeliveryOrderService(
.associateBy { it.id }

val preFilteredContent = allResult.content
val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()

// ✅ 优化3: 收集所有需要查询的 shopId 和日期组合(只处理预过滤后的记录)
val shopIdAndDatePairs = preFilteredContent.mapNotNull { info ->
@@ -191,11 +191,7 @@ open class DeliveryOrderService(
val targetDate = estimatedArrivalDate.toLocalDate()
val dayAbbr = getDayOfWeekAbbr(targetDate)
val supplierCode = deliveryOrder.supplier?.code
val preferredFloor = when (supplierCode) {
"P06B" -> "4F"
"P07", "P06D" -> "2F"
else -> "2F" // 或者改成 null / 其他默认值,看你业务需要
}
val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
Triple(shopId, preferredFloor, dayAbbr)
} else {
null
@@ -217,11 +213,7 @@ open class DeliveryOrderService(
val processedRecords = preFilteredContent.map { info ->
val deliveryOrder = deliveryOrdersMap[info.id]
val supplierCode = deliveryOrder?.supplier?.code
val preferredFloor = when (supplierCode) {
"P06B" -> "4F"
"P07", "P06D" -> "2F"
else -> "2F"
}
val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
val shop = deliveryOrder?.shop
val shopId = shop?.id
val estimatedArrivalDate = info.estimatedArrivalDate
@@ -248,7 +240,7 @@ open class DeliveryOrderService(
supplierName = info.supplierName,
shopAddress = info.shopAddress,
truckLanceCode = calculatedTruckLanceCode,
isEtra = deliveryOrdersMap[info.id]?.isEtra ?: info.isEtra,
isExtra = deliveryOrdersMap[info.id]?.isExtra ?: info.isExtra,
)
}.filter { dto ->
val dtoTruckLanceCode = dto.truckLanceCode?.lowercase() ?: ""
@@ -279,19 +271,16 @@ open class DeliveryOrderService(
status = statusEnum,
etaStart = etaStart,
etaEnd = etaEnd,
isEtra = isEtra,
isExtra = isExtra,
allowedSupplierCodes = allowedSupplierCodes,
pageable = PageRequest.of(page.coerceAtLeast(0), size),
)

val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()
val records = result.content.map { info ->
val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(info.id)
val supplierCode = deliveryOrder?.supplier?.code
val preferredFloor = when (supplierCode) {
"P06B" -> "4F"
"P07", "P06D" -> "2F"
else -> "2F"
}
val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
val shop = deliveryOrder?.shop
val shopId = shop?.id
val estimatedArrivalDate = info.estimatedArrivalDate
@@ -315,7 +304,7 @@ open class DeliveryOrderService(
supplierName = info.supplierName,
shopAddress = info.shopAddress,
truckLanceCode = calculatedTruckLanceCode,
isEtra = deliveryOrder?.isEtra ?: info.isEtra,
isExtra = deliveryOrder?.isExtra ?: info.isExtra,
)
}

@@ -338,7 +327,7 @@ open class DeliveryOrderService(
pageSize: Int?,
truckLanceCode: String?,
floor: String? = null,
isEtra: Boolean? = null,
isExtra: Boolean? = null,
): RecordsRes<DeliveryOrderInfoLiteDto> {
val mode = TruckLaneSearchSpec.parse(truckLanceCode)
if (mode is TruckLaneSearchSpec.Mode.NoFilter) {
@@ -351,7 +340,7 @@ open class DeliveryOrderService(
pageSize,
null,
floor,
isEtra,
isExtra,
)
}
val pageIdx = (pageNum ?: 1).coerceAtLeast(1) - 1
@@ -367,7 +356,7 @@ open class DeliveryOrderService(
statusEnum = statusEnum,
etaStart = etaStart,
etaEnd = etaEnd,
isEtra = isEtra,
isExtra = isExtra,
allowedSupplierCodes = allowedSupplierCodesForFloor(floor),
lanePredicate = lanePredicate,
)
@@ -391,7 +380,7 @@ open class DeliveryOrderService(
pageNum: Int?,
pageSize: Int?,
floor: String? = null,
isEtra: Boolean? = null,
isExtra: Boolean? = null,
): RecordsRes<DeliveryOrderInfoLiteDto> {
val page = (pageNum ?: 1) - 1
val size = pageSize ?: 10
@@ -406,22 +395,19 @@ open class DeliveryOrderService(
status = statusEnum,
etaStart = etaStart,
etaEnd = etaEnd,
isEtra = isEtra,
isExtra = isExtra,
allowedSupplierCodes = allowedSupplierCodes,
pageable = PageRequest.of(0, 100_000),
)

val deliveryOrderIds = allResult.content.mapNotNull { it.id }
val deliveryOrdersMap = deliveryOrderRepository.findAllById(deliveryOrderIds).associateBy { it.id }
val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()

val processedRecords = allResult.content.map { info ->
val deliveryOrder = deliveryOrdersMap[info.id]
val supplierCode = deliveryOrder?.supplier?.code
val preferredFloor = when (supplierCode) {
"P06B" -> "4F"
"P07", "P06D" -> "2F"
else -> "2F"
}
val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
val shop = deliveryOrder?.shop
val shopId = shop?.id
val infoEta = info.estimatedArrivalDate
@@ -445,7 +431,7 @@ open class DeliveryOrderService(
supplierName = info.supplierName,
shopAddress = info.shopAddress,
truckLanceCode = calculatedTruckLanceCode,
isEtra = deliveryOrdersMap[info.id]?.isEtra ?: info.isEtra,
isExtra = deliveryOrdersMap[info.id]?.isExtra ?: info.isExtra,
)
}.filter { dto -> TruckLaneSearchSpec.isUnassignedResolvedLane(dto.truckLanceCode) }

@@ -487,7 +473,7 @@ open class DeliveryOrderService(
estimatedArrivalDate = deliveryOrder.estimatedArrivalDate,
completeDate = deliveryOrder.completeDate,
status = deliveryOrder.status?.value,
isEtra = deliveryOrder.isEtra,
isExtra = deliveryOrder.isExtra,
deliveryOrderLines = deliveryOrder.deliveryOrderLines.map { line ->
DoDetailLineResponse(
id = line.id!!,
@@ -808,7 +794,7 @@ open class DeliveryOrderService(
this.handler = handler
m18BeId = request.m18BeId
this.deleted = request.deleted
isEtra = request.isEtra ?: false
isExtra = request.isExtra ?: false
}

val savedDeliveryOrder = deliveryOrderRepository.saveAndFlush(deliveryOrder).let {
@@ -948,14 +934,10 @@ open class DeliveryOrderService(

println(" DEBUG: Target date: $targetDate, Date prefix: $datePrefix")
// 新逻辑:根据 supplier code 决定楼层
// 如果 supplier code 是 "P06B",使用 4F,否则使用 2F
// 新逻辑:根据 supplier code 决定楼层(清單來自 settings)
val supplierCode = deliveryOrder.supplier?.code
val preferredFloor = when (supplierCode) {
"P06B" -> "4F"
"P07", "P06D" -> "2F"
else -> "2F" // 或者改成 null / 其他默认值,看你业务需要
}
val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()
val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)

println(" DEBUG: Supplier code: $supplierCode, Preferred floor: $preferredFloor")

@@ -1839,15 +1821,11 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
}
}

// 新逻辑:根据 supplier code 决定楼层
// 如果 supplier code 是 "P06B",使用 4F,否则使用 2F
// 新逻辑:根据 supplier code 决定楼层(清單來自 settings)
val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now()
val supplierCode = deliveryOrder.supplier?.code
val preferredFloor = when (supplierCode) {
"P06B" -> "4F"
"P07", "P06D" -> "2F"
else -> "2F" // 或者改成 null / 其他默认值,看你业务需要
}
val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()
val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)

println(" DEBUG: Floor calculation for DO ${deliveryOrder.id}")
println(" - Supplier code: $supplierCode")
@@ -1936,7 +1914,8 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
truckDepartureTime = effectiveTruck.departureTime,
truckLanceCode = effectiveTruck.truckLanceCode,
loadingSequence = effectiveTruck.loadingSequence,
usedDefaultTruck = usedDefaultTruck
usedDefaultTruck = usedDefaultTruck,
isExtra = deliveryOrder.isExtra ?: false,

)
}
@@ -2022,11 +2001,8 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
// Truck selection (reuse normal logic)
val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now()
val supplierCode = deliveryOrder.supplier?.code
val preferredFloor = when (supplierCode) {
"P06B" -> "4F"
"P07", "P06D" -> "2F"
else -> "2F"
}
val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()
val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)

val truck = deliveryOrder.shop?.id?.let { shopId ->
val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
@@ -2094,7 +2070,8 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
truckDepartureTime = effectiveTruck.departureTime,
truckLanceCode = effectiveTruck.truckLanceCode,
loadingSequence = effectiveTruck.loadingSequence,
usedDefaultTruck = usedDefaultTruck
usedDefaultTruck = usedDefaultTruck,
isExtra = deliveryOrder.isExtra ?: false,
)
}

@@ -2104,7 +2081,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
statusEnum: DeliveryOrderStatus?,
etaStart: LocalDateTime?,
etaEnd: LocalDateTime?,
isEtra: Boolean?,
isExtra: Boolean?,
allowedSupplierCodes: List<String>,
lanePredicate: (String?) -> Boolean,
): List<DeliveryOrderInfoLiteDto> {
@@ -2118,7 +2095,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
status = statusEnum,
etaStart = etaStart,
etaEnd = etaEnd,
isEtra = isEtra,
isExtra = isExtra,
allowedSupplierCodes = allowedSupplierCodes,
pageable = PageRequest.of(dbPage, 500),
)
@@ -2140,6 +2117,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
val ids = rows.mapNotNull { it.id }
if (ids.isEmpty()) return emptyList()
val deliveryOrdersMap = deliveryOrderRepository.findAllById(ids).associateBy { it.id }
val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()

val shopIdAndDatePairs = rows.mapNotNull { info ->
val d = deliveryOrdersMap[info.id]
@@ -2149,11 +2127,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
val targetDate = eta.toLocalDate()
val dayAbbr = getDayOfWeekAbbr(targetDate)
val supplierCode = d.supplier?.code
val preferredFloor = when (supplierCode) {
"P06B" -> "4F"
"P07", "P06D" -> "2F"
else -> "2F"
}
val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
Triple(shopId, preferredFloor, dayAbbr)
} else {
null
@@ -2169,11 +2143,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
return rows.map { info ->
val deliveryOrder = deliveryOrdersMap[info.id]
val supplierCode = deliveryOrder?.supplier?.code
val preferredFloor = when (supplierCode) {
"P06B" -> "4F"
"P07", "P06D" -> "2F"
else -> "2F"
}
val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
val shopId = deliveryOrder?.shop?.id
val infoEta = info.estimatedArrivalDate
val calculatedTruckLanceCode =
@@ -2194,14 +2164,14 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
supplierName = info.supplierName,
shopAddress = info.shopAddress,
truckLanceCode = calculatedTruckLanceCode,
isEtra = deliveryOrder?.isEtra ?: info.isEtra,
isExtra = deliveryOrder?.isExtra ?: info.isExtra,
)
}
}

/**
* 依店鋪 + 揀貨樓層解析當日應顯示之車線。
* - **2F**(P07/P06D):`TruckLanceCode` 多為 `車線-…` 且不含星期縮寫,不依 `LIKE '%Mon%'` 篩選;取該店該樓層未刪除車輛中出發時間最早者。
* - **2F**(P07/P06D/P06Y):`TruckLanceCode` 多為 `車線-…` 且不含星期縮寫,不依 `LIKE '%Mon%'` 篩選;取該店該樓層未刪除車輛中出發時間最早者。
* - **4F**(P06B):維持以星期縮寫篩選 [TruckRepository.findByShopIdAndStoreIdAndDayOfWeek];無命中時再退回同樓層最早出發。
*/
private fun resolveTruckForShopFloorAndDay(


+ 95
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt Zobrazit soubor

@@ -0,0 +1,95 @@
package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.fpsms.modules.settings.entity.SettingsRepository
import org.springframework.stereotype.Service
import java.util.Locale

/** 供 DO 搜尋/車線/報表 SQL 等共用的 2F/4F 供應商代碼(來自 `settings` CSV)。 */
@Service
open class DoFloorSupplierSettingsService(
private val settingsRepository: SettingsRepository,
) {
companion object {
private const val SETTING_DO_FLOOR_SUPPLIERS_2F = "DO.floor.suppliers.2F"
private const val SETTING_DO_FLOOR_SUPPLIERS_4F = "DO.floor.suppliers.4F"

private val DEFAULT_SUPPLIERS_2F = listOf("P07", "P06D", "P06Y")
private val DEFAULT_SUPPLIERS_4F = listOf("P06B")
}

open fun supplierCodesFromSetting(settingName: String, defaultList: List<String>): List<String> {
val raw = settingsRepository.findByName(settingName).map { it.value }.orElse(null)
?.trim()
.orEmpty()
if (raw.isEmpty()) return defaultList
val parsed = raw.split(",").map { it.trim() }.filter { it.isNotEmpty() }.distinct()
return parsed.ifEmpty { defaultList }
}

open fun loadDoFloorSupplierLists(): Pair<List<String>, List<String>> {
val suppliers2F = supplierCodesFromSetting(SETTING_DO_FLOOR_SUPPLIERS_2F, DEFAULT_SUPPLIERS_2F)
val suppliers4F = supplierCodesFromSetting(SETTING_DO_FLOOR_SUPPLIERS_4F, DEFAULT_SUPPLIERS_4F)
return suppliers2F to suppliers4F
}

open fun allowedSupplierCodesForFloor(floor: String?): List<String> {
val f = floor?.trim()?.uppercase(Locale.ROOT).orEmpty()
val (codes2F, codes4F) = loadDoFloorSupplierLists()
return when {
f.isEmpty() || f == "ALL" || f == "All" -> (codes2F + codes4F).distinct()
f == "2F" -> codes2F
f == "4F" -> codes4F
else -> (codes2F + codes4F).distinct()
}
}

/** 4F 清單優先;其餘預設 2F(與既有 DO 車線邏輯一致)。 */
open fun preferredStoreFloorForSupplier(
supplierCode: String?,
suppliers2F: List<String>,
suppliers4F: List<String>,
): String {
val code = supplierCode?.trim().orEmpty()
if (code.isEmpty()) return "2F"
if (suppliers4F.contains(code)) return "4F"
if (suppliers2F.contains(code)) return "2F"
return "2F"
}

/** DO 揀貨建議:名單外供應商不限制 2F/4F。 */
open fun preferredFloorForPickLotOrNull(
supplierCode: String?,
suppliers2F: List<String>,
suppliers4F: List<String>,
): String? {
val code = supplierCode?.trim().orEmpty()
if (code.isEmpty()) return null
if (suppliers4F.contains(code)) return "4F"
if (suppliers2F.contains(code)) return "2F"
return null
}

data class SqlPreferredFloorCases(
/** 例如 `CASE WHEN s.code IN (...) THEN '4F' ... END`(單行,可嵌入原生 SQL) */
val floorStringCase: String,
val storeIdNumericCase: String,
)

/**
* 依目前 settings 產生原生 SQL CASE(供 JDBC 字串拼接)。
* @param codeExpr 已加別名的欄位,如 `s.code`、`supplier.code`
*/
open fun sqlPreferredFloorCases(codeExpr: String = "s.code"): SqlPreferredFloorCases {
val (s2f, s4f) = loadDoFloorSupplierLists()
val in4 = joinSqlInList(s4f)
val in2 = joinSqlInList(s2f)
val floor =
"CASE WHEN $codeExpr IN ($in4) THEN '4F' WHEN $codeExpr IN ($in2) THEN '2F' ELSE NULL END"
val storeId =
"CASE WHEN $codeExpr IN ($in4) THEN 4 WHEN $codeExpr IN ($in2) THEN 2 ELSE NULL END"
return SqlPreferredFloorCases(floorStringCase = floor, storeIdNumericCase = storeId)
}

private fun joinSqlInList(codes: List<String>): String =
codes.joinToString(", ") { "'" + it.replace("'", "''") + "'" }
}

+ 7
- 20
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt Zobrazit soubor

@@ -103,6 +103,7 @@ class DoReleaseCoordinatorService(
private val userRepository: UserRepository,
private val pickOrderRepository: PickOrderRepository,
private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService,
) {
private val poolSize = Runtime.getRuntime().availableProcessors()
private val executor = Executors.newFixedThreadPool(min(poolSize, 4))
@@ -140,22 +141,15 @@ class DoReleaseCoordinatorService(
private fun updateBatchTicketNumbers() {
try {
val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate")
val pfCases = doFloorSupplierSettingsService.sqlPreferredFloorCases("s.code")
val updateSql = """
UPDATE fpsmsdb.do_pick_order dpo
INNER JOIN (
WITH PreferredFloor AS (
SELECT
do.id AS deliveryOrderId,
CASE
WHEN s.code = 'P06B' THEN '4F'
WHEN s.code = 'P07' OR s.code = 'P06D' THEN '2F'
ELSE NULL
END AS preferred_floor,
CASE
WHEN s.code = 'P06B' THEN 4
WHEN s.code = 'P07' OR s.code = 'P06D' THEN 2
ELSE NULL
END AS preferred_store_id
${pfCases.floorStringCase} AS preferred_floor,
${pfCases.storeIdNumericCase} AS preferred_store_id
FROM fpsmsdb.delivery_order do
LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0
WHERE do.deleted = 0
@@ -307,20 +301,13 @@ class DoReleaseCoordinatorService(
println(" DEBUG: Getting ordered IDs for ${ids.size} orders")
println(" DEBUG: First 5 IDs: ${ids.take(5)}")
val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate")
val pfCases = doFloorSupplierSettingsService.sqlPreferredFloorCases("s.code")
val sql = """
WITH PreferredFloor AS (
SELECT
do.id AS deliveryOrderId,
CASE
WHEN s.code = 'P06B' THEN '4F'
WHEN s.code = 'P07' OR s.code = 'P06D' THEN '2F'
ELSE NULL
END AS preferred_floor,
CASE
WHEN s.code = 'P06B' THEN 4
WHEN s.code = 'P07' OR s.code = 'P06D' THEN 2
ELSE NULL
END AS preferred_store_id
${pfCases.floorStringCase} AS preferred_floor,
${pfCases.storeIdNumericCase} AS preferred_store_id
FROM fpsmsdb.delivery_order do
LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0
WHERE do.id IN (${ids.joinToString(",")})


+ 11
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchDopoAssignmentService.kt Zobrazit soubor

@@ -144,6 +144,9 @@ open class DoWorkbenchDopoAssignmentService(
sql.append(" AND dop.loadingSequence = :loadingSequence ")
params["loadingSequence"] = request.loadingSequence
}
if (isisExtraReleaseType(request.releaseType)) {
sql.append(" AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra' ")
}
// Fetch a batch of candidates and try atomic-assign sequentially.
// This avoids forcing the frontend to refresh when a single picked candidate is concurrently assigned.
val candidateLimit = 50
@@ -247,6 +250,9 @@ open class DoWorkbenchDopoAssignmentService(
sql.append(" AND dop.loadingSequence = :loadingSequence ")
params["loadingSequence"] = request.loadingSequence
}
if (isisExtraReleaseType(request.releaseType)) {
sql.append(" AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra' ")
}
val shouldOrderBySequenceV1 = actualStoreId == "2/F" && request.loadingSequence == null
if (shouldOrderBySequenceV1) {
sql.append(" ORDER BY dop.requiredDeliveryDate ASC, dop.truckDepartureTime ASC, dop.loadingSequence ASC, dop.id ASC LIMIT 1 ")
@@ -301,6 +307,11 @@ open class DoWorkbenchDopoAssignmentService(
} else null
}

private fun isisExtraReleaseType(releaseType: String?): Boolean {
val n = releaseType?.trim()?.lowercase().orEmpty()
return n == "isExtra"
}

private fun parseDepartureTimeToSql(raw: String?): Time? {
if (raw.isNullOrBlank()) return null
val s = raw.trim()


+ 205
- 11
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt Zobrazit soubor

@@ -1,3 +1,4 @@

package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.core.support.JdbcDao
@@ -54,6 +55,7 @@ import java.time.format.DateTimeFormatter
import com.ffii.fpsms.modules.deliveryOrder.web.models.StoreLaneSummary
import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneRow
import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneBtn
import com.ffii.fpsms.modules.deliveryOrder.web.models.WorkbenchEtraShopLaneGroup
import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleasedDoPickOrderListItem
import com.ffii.fpsms.modules.deliveryOrder.web.models.WorkbenchTicketReleaseTableResponse
import com.ffii.fpsms.modules.user.service.UserService
@@ -670,6 +672,7 @@ return MessageResponse(
val releaseFilterClause = when (rt) {
"batch" -> " AND LOWER(COALESCE(dop.releaseType, '')) = 'batch' "
"single" -> " AND LOWER(COALESCE(dop.releaseType, '')) = 'single' "
"isExtra" -> " AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra' "
else -> ""
}
val sql = """
@@ -812,6 +815,7 @@ return MessageResponse(
unassigned = it.unassigned,
total = it.total,
handlerName = it.handlerName,
storeId = actualStoreId,
)
}
.sortedWith(
@@ -853,24 +857,181 @@ return MessageResponse(
)
}

/**
* Workbench Etra view: all `delivery_order_pick_order` with `releaseType` = isExtra (case-insensitive),
* for one [requiredDeliveryDate], grouped by shop then by truck / time / loading sequence.
*/
open fun getWorkbenchEtraLaneSummary(requiredDate: LocalDate?): List<WorkbenchEtraShopLaneGroup> {
val targetDate = requiredDate ?: LocalDate.now()
val defaultTruck = truckRepository.findById(5577L).orElse(null)
val defaultTruckLaneCode = defaultTruck?.truckLanceCode ?: ""

val sql = """
SELECT
dop.shopCode AS shopCode,
dop.shopName AS shopName,
dop.storeId AS storeId,
dop.truckDepartureTime AS truckDepartureTime,
dop.truckLanceCode AS truckLanceCode,
dop.loadingSequence AS loadingSequence,
COUNT(DISTINCT dop.id) AS total_cnt,
SUM(CASE WHEN dop.handledBy IS NULL THEN 1 ELSE 0 END) AS unassigned_cnt,
GROUP_CONCAT(
DISTINCT NULLIF(TRIM(dop.handlerName), '')
ORDER BY dop.handlerName
SEPARATOR ', '
) AS handler_names
FROM fpsmsdb.delivery_order_pick_order dop
WHERE dop.deleted = 0
AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra'
AND dop.requiredDeliveryDate = :requiredDate
AND dop.ticketStatus IN ('pending', 'released')
AND EXISTS (
SELECT 1 FROM fpsmsdb.pick_order po
WHERE po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0
)
GROUP BY dop.shopCode, dop.shopName, dop.storeId, dop.truckDepartureTime, dop.truckLanceCode, dop.loadingSequence
""".trimIndent()

val rawRows: List<Map<String, Any?>> = try {
jdbcDao.queryForList(sql, mapOf("requiredDate" to targetDate))
} catch (e: Exception) {
println("❌ getWorkbenchEtraLaneSummary: ${e.message}")
emptyList()
}

fun cellStr(row: Map<String, Any?>, name: String): String? {
val k = row.keys.find { it.equals(name, true) } ?: return null
return row[k]?.toString()?.trim()?.takeIf { it.isNotEmpty() }
}
fun cellNum(row: Map<String, Any?>, vararg names: String): Int {
for (n in names) {
val k = row.keys.find { it.equals(n, true) } ?: continue
(row[k] as? Number)?.toInt()?.let { return it }
}
return 0
}
fun cellNullableInt(row: Map<String, Any?>, vararg names: String): Int? {
for (n in names) {
val k = row.keys.find { it.equals(n, true) } ?: continue
val v = row[k] ?: continue
when (v) {
is Number -> return v.toInt()
is String -> v.trim().toIntOrNull()?.let { return it }
}
}
return null
}

data class EtraAgg(
val shopCode: String?,
val shopName: String?,
val storeId: String?,
val sortTime: LocalTime,
val lance: String,
val loadingSequence: Int?,
val unassigned: Int,
val total: Int,
val handlerName: String?,
)

val aggs = rawRows.mapNotNull { row ->
val lance = cellStr(row, "truckLanceCode") ?: return@mapNotNull null
if (lance == defaultTruckLaneCode) return@mapNotNull null
val storeIdCol = cellStr(row, "storeId")
val ttKey = row.keys.find { it.equals("truckDepartureTime", true) }
val ttVal = ttKey?.let { row[it] }
val sortTime = when (ttVal) {
null -> LocalTime.MIDNIGHT
is java.sql.Time -> ttVal.toLocalTime()
is LocalTime -> ttVal
is java.sql.Timestamp -> ttVal.toLocalDateTime().toLocalTime()
else -> runCatching { LocalTime.parse(ttVal.toString().take(8)) }.getOrNull()
?: runCatching { LocalTime.parse(ttVal.toString()) }.getOrNull()
?: LocalTime.MIDNIGHT
}
val loadingSeq = cellNullableInt(row, "loadingSequence")
val unassigned = cellNum(row, "unassigned_cnt", "unassignedCnt")
val total = cellNum(row, "total_cnt", "totalCnt")
if (total <= 0) return@mapNotNull null
EtraAgg(
shopCode = cellStr(row, "shopCode"),
shopName = cellStr(row, "shopName"),
storeId = storeIdCol,
sortTime = sortTime,
lance = lance,
loadingSequence = loadingSeq,
unassigned = unassigned,
total = total,
handlerName = cellStr(row, "handler_names"),
)
}

val byShop = aggs.groupBy { a ->
listOf(a.shopCode ?: "", a.shopName ?: "").joinToString("|")
}

return byShop.entries
.map { (key, group) ->
val head = group.first()
val lanes = group
.sortedWith(
compareBy<EtraAgg> { it.sortTime }
.thenBy { it.lance }
.thenBy { it.loadingSequence ?: 999 }
)
.map {
val is4F = it.storeId?.replace("/", "")?.trim()?.equals("4F", ignoreCase = true) == true
LaneBtn(
truckLanceCode = it.lance,
loadingSequence = if (is4F) it.loadingSequence else null,
unassigned = it.unassigned,
total = it.total,
handlerName = it.handlerName,
storeId = it.storeId,
truckDepartureTime = it.sortTime.toString(),
)
}
WorkbenchEtraShopLaneGroup(
shopCode = head.shopCode,
shopName = head.shopName,
lanes = lanes,
)
}
.sortedWith(
compareBy<WorkbenchEtraShopLaneGroup> { it.shopName ?: it.shopCode ?: "" }
.thenBy { it.shopCode ?: "" }
)
}

open fun findWorkbenchReleasedDeliveryOrderPickOrdersForSelection(
shopName: String?,
storeId: String?,
truck: String?,
releaseTypeFilter: String? = null,
): List<ReleasedDoPickOrderListItem> =
queryWorkbenchReleasedDopoList(shopName, storeId, truck, beforeToday = true)
queryWorkbenchReleasedDopoList(shopName, storeId, truck, beforeToday = true, releaseTypeFilter = releaseTypeFilter)

/**
* @param requiredDeliveryDate when null, uses [LocalDate.now] (calendar today).
* When set, filters `dop.requiredDeliveryDate = :targetDate` (workbench date picker / select day).
* @param releaseTypeFilter when `isExtra` (case-insensitive), only `delivery_order_pick_order.releaseType = isExtra` rows.
*/
open fun findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday(
shopName: String?,
storeId: String?,
truck: String?,
requiredDeliveryDate: LocalDate? = null,
releaseTypeFilter: String? = null,
): List<ReleasedDoPickOrderListItem> =
queryWorkbenchReleasedDopoList(shopName, storeId, truck, beforeToday = false, equalsDeliveryDate = requiredDeliveryDate)
queryWorkbenchReleasedDopoList(
shopName,
storeId,
truck,
beforeToday = false,
equalsDeliveryDate = requiredDeliveryDate,
releaseTypeFilter = releaseTypeFilter,
)

/**
* Workbench completed tickets: query `delivery_order_pick_order` where `ticketStatus = completed`.
@@ -1362,8 +1523,9 @@ return MessageResponse(
dop.deliveryNoteCode = CodeGenerator.generateNo(prefix = prefix, midfix = midfix, latestCode = latestCode)
}
deliveryOrderPickOrderRepository.save(dop)
}
markDeliveryOrdersCompletedForDeliveryOrderPickOrder(deliveryOrderPickOrderId)
return MessageResponse(
id = dop.id,
code = "SUCCESS",
@@ -1468,6 +1630,7 @@ return MessageResponse(
truck: String?,
beforeToday: Boolean,
equalsDeliveryDate: LocalDate? = null,
releaseTypeFilter: String? = null,
): List<ReleasedDoPickOrderListItem> {
val today = LocalDate.now()
val params = mutableMapOf<String, Any>()
@@ -1518,6 +1681,10 @@ return MessageResponse(
sqlBuilder.append(" AND (dop.shopName LIKE :shopPat OR dop.shopCode LIKE :shopPat) ")
params["shopPat"] = "%${shopName.trim()}%"
}
val rtNorm = releaseTypeFilter?.trim()?.lowercase().orEmpty()
if (rtNorm == "isExtra") {
sqlBuilder.append(" AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra' ")
}
sqlBuilder.append(" ORDER BY dop.requiredDeliveryDate, dop.truckDepartureTime, dop.truckLanceCode, dop.id ")
val rows: List<Map<String, Any?>> = try {
jdbcDao.queryForList(sqlBuilder.toString(), params)
@@ -1912,6 +2079,7 @@ return MessageResponse(
tryCompleteDeliveryOrderPickOrderTicketCompleted(poId)
}
}

private fun registerAfterCommit(action: () -> Unit) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
action()
@@ -2047,6 +2215,7 @@ return MessageResponse(
)
}
}

private fun postWorkbenchPickSideEffects(savedStockOutLine: StockOutLine, deltaQty: BigDecimal, createLedger: Boolean = true) {
if (deltaQty <= BigDecimal.ZERO) return
val wall0 = System.nanoTime()
@@ -2229,9 +2398,10 @@ return MessageResponse(
throw last ?: RuntimeException("Failed to complete pick order after retries (poId=$poId)")
}

/**
/**
* Workbench completion: if all pick_orders under the same delivery_order_pick_order are completed,
* update ONLY delivery_order_pick_order.ticketStatus (no do_pick_order/do_pick_order_line records).
* update delivery_order_pick_order.ticketStatus and related delivery_order.status → completed.
* Does not create do_pick_order / do_pick_order_line records.
*/
private fun tryCompleteDeliveryOrderPickOrderTicketCompleted(poId: Long) {
val dopRow = jdbcDao.queryForMap(
@@ -2276,8 +2446,36 @@ return MessageResponse(
""".trimIndent(),
mapOf("dopId" to dopId, "deliveryNoteCode" to newDeliveryNoteCode),
)
markDeliveryOrdersCompletedForDeliveryOrderPickOrder(dopId)
}
/**
* When a workbench ticket ([delivery_order_pick_order]) is completed, align linked [delivery_order] headers.
*/
private fun markDeliveryOrdersCompletedForDeliveryOrderPickOrder(dopId: Long) {
if (dopId <= 0L) return
val rows = try {
jdbcDao.queryForList(
"""
SELECT DISTINCT po.doId AS doId
FROM fpsmsdb.pick_order po
WHERE po.deliveryOrderPickOrderId = :dopId
AND po.deleted = 0
AND po.doId IS NOT NULL
""".trimIndent(),
mapOf("dopId" to dopId),
)
} catch (_: Exception) {
emptyList()
}
for (row in rows) {
val doId = (row["doId"] as? Number)?.toLong() ?: continue
val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId) ?: continue
if (deliveryOrder.status == DeliveryOrderStatus.COMPLETED) continue
deliveryOrder.status = DeliveryOrderStatus.COMPLETED
deliveryOrder.completeDate = LocalDateTime.now()
deliveryOrderRepository.save(deliveryOrder)
}
}

private fun checkWorkbenchPickOrderLineCompleted(pickOrderLineId: Long, allStockOutLines: List<StockOutLineInfo>) {
val pol = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) ?: return
if (pol.status == PickOrderLineStatus.COMPLETED) return
@@ -2715,11 +2913,7 @@ return MessageResponse(
}
}

/**
* Carton label reprint for workbench: [request.doPickOrderId] is [delivery_order_pick_order.id],
* same as [getWorkbenchPrintContext]. Legacy [DeliveryOrderService.printDNLabelsReprint] expects
* [do_pick_order_record.recordId] and must not be used here.
*/

private fun exportDNLabelsReprintWorkbench(request: PrintDNLabelsReprintRequest): Map<String, Any> {
validateWorkbenchCartonReprintRange(
fromCarton = request.fromCarton,


+ 20
- 9
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt Zobrazit soubor

@@ -359,14 +359,17 @@ open class DoWorkbenchReleaseService(
}

/**
* `TI-B-yyyyMMdd-2F-001` (batch) or `TI-S-yyyyMMdd-2F-001` (single), same suffix rules as [DoReleaseCoordinatorService] / legacy `do_pick_order`.
* `TI-B-yyyyMMdd-2F-001` (batch), `TI-S-yyyyMMdd-2F-001` (single), or `TI-E-yyyyMMdd-2F-001` (Etra),
* same suffix rules as [DoReleaseCoordinatorService] / legacy `do_pick_order`.
*/
private fun nextDeliveryOrderPickOrderTicketNo(
requiredDate: LocalDate,
storeDisplay: String,
ticketLetter: String,
): String {
require(ticketLetter == "B" || ticketLetter == "S") { "ticketLetter must be B or S" }
require(ticketLetter == "B" || ticketLetter == "S" || ticketLetter == "E") {
"ticketLetter must be B, S or E"
}
val ymd = requiredDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
val floor = storeDisplay.replace("/", "").trim()
val prefix = "TI-$ticketLetter-$ymd-$floor-"
@@ -397,6 +400,9 @@ open class DoWorkbenchReleaseService(
private fun nextDeliveryOrderPickOrderSingleTicketNo(requiredDate: LocalDate, storeDisplay: String): String =
nextDeliveryOrderPickOrderTicketNo(requiredDate, storeDisplay, "S")

private fun nextDeliveryOrderPickOrderEtraTicketNo(requiredDate: LocalDate, storeDisplay: String): String =
nextDeliveryOrderPickOrderTicketNo(requiredDate, storeDisplay, "E")

private fun asyncJobType(useV2: Boolean, dopReleaseType: String): String {
val single = dopReleaseType.equals("single", ignoreCase = true)
return when {
@@ -440,11 +446,6 @@ open class DoWorkbenchReleaseService(
): Int {
if (results.isEmpty()) return 0

val releaseTypeCol = when (dopReleaseType.lowercase()) {
"single" -> "single"
else -> "batch"
}

val grouped = results.groupBy {
listOf(
it.shopId?.toString() ?: "",
@@ -452,7 +453,8 @@ open class DoWorkbenchReleaseService(
it.preferredFloor,
it.truckId?.toString() ?: "",
it.truckDepartureTime?.toString() ?: "",
it.truckLanceCode ?: ""
it.truckLanceCode ?: "",
it.isExtra.toString(),
).joinToString("|")
}

@@ -477,7 +479,16 @@ open class DoWorkbenchReleaseService(
(storeId ?: "2/F").replace("/", "").trim()
}
val requiredDate = first.estimatedArrivalDate ?: LocalDate.now()
val tempTicket = if (releaseTypeCol == "single") {
val releaseTypeCol = if (first.isExtra) {
"isExtra"
} else if (dopReleaseType.equals("single", ignoreCase = true)) {
"single"
} else {
"batch"
}
val tempTicket = if (first.isExtra) {
nextDeliveryOrderPickOrderEtraTicketNo(requiredDate, ticketFloorSegment)
} else if (releaseTypeCol == "single") {
nextDeliveryOrderPickOrderSingleTicketNo(requiredDate, ticketFloorSegment)
} else {
nextDeliveryOrderPickOrderBatchTicketNo(requiredDate, ticketFloorSegment)


+ 3
- 3
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt Zobrazit soubor

@@ -72,7 +72,7 @@ class DeliveryOrderController(
pageSize = request.pageSize,
truckLanceCode = request.truckLanceCode,
floor = request.floor,
isEtra = request.isEtra,
isExtra = request.isExtra,
)
}

@@ -89,7 +89,7 @@ class DeliveryOrderController(
pageNum = request.pageNum,
pageSize = request.pageSize,
floor = request.floor,
isEtra = request.isEtra,
isExtra = request.isExtra,
)
}

@@ -108,7 +108,7 @@ class DeliveryOrderController(
pageSize = request.pageSize,
truckLanceCode = request.truckLanceCode,
floor = request.floor,
isEtra = request.isEtra,
isExtra = request.isExtra,
)
}



+ 17
- 2
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoWorkbenchController.kt Zobrazit soubor

@@ -96,14 +96,27 @@ class DoWorkbenchController(
)
}

/** All Etra workbench tickets for a day, grouped by shop → truck (see [DoWorkbenchMainService.getWorkbenchEtraLaneSummary]). */
@GetMapping("/summary-is-etra")
fun getWorkbenchEtraSummary(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate?,
): List<WorkbenchEtraShopLaneGroup> =
doWorkbenchMainService.getWorkbenchEtraLaneSummary(requiredDate)

/** Past-date backlog tickets from `delivery_order_pick_order` (not `do_pick_order`). */
@GetMapping("/released")
fun getWorkbenchReleasedDoPickOrders(
@RequestParam(required = false) shopName: String?,
@RequestParam(required = false) storeId: String?,
@RequestParam(required = false) truck: String?
@RequestParam(required = false) truck: String?,
@RequestParam(required = false) releaseType: String?,
): List<ReleasedDoPickOrderListItem> {
return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelection(shopName, storeId, truck)
return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelection(
shopName,
storeId,
truck,
releaseTypeFilter = releaseType,
)
}

@GetMapping("/released-today")
@@ -112,12 +125,14 @@ class DoWorkbenchController(
@RequestParam(required = false) storeId: String?,
@RequestParam(required = false) truck: String?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate?,
@RequestParam(required = false) releaseType: String?,
): List<ReleasedDoPickOrderListItem> {
return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday(
shopName,
storeId,
truck,
requiredDeliveryDate = requiredDate,
releaseTypeFilter = releaseType,
)
}



+ 16
- 3
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt Zobrazit soubor

@@ -19,7 +19,7 @@ data class DoDetailResponse(
val completeDate: LocalDateTime?,
val status: String?,
/** 加單 DO(M18 加單專用同步) */
val isEtra: Boolean = false,
val isExtra: Boolean = false,
val deliveryOrderLines: List<DoDetailLineResponse>
)

@@ -51,7 +51,18 @@ data class LaneBtn(
val unassigned: Int,
val total: Int,
// 同一 truckLanceCode + loadingSequence 的 handler 去重后逗号拼接
val handlerName: String? = null
val handlerName: String? = null,
/** Workbench Etra lane: `delivery_order_pick_order.storeId` (2/F, 4/F, …) for assign / modal scope */
val storeId: String? = null,
/** Workbench Etra / lane row: `truckDepartureTime` as ISO local time string for assign-by-lane */
val truckDepartureTime: String? = null,
)

/** All Etra (`releaseType=isExtra`) tickets for a day, grouped by shop then truck (no 2F/4F split in UI). */
data class WorkbenchEtraShopLaneGroup(
val shopCode: String?,
val shopName: String?,
val lanes: List<LaneBtn>,
)
data class AssignByLaneRequest(
val userId: Long,
@@ -59,7 +70,9 @@ data class AssignByLaneRequest(
val truckDepartureTime: String?, // 可选:限定出车时间
val truckLanceCode: String ,
val loadingSequence: Int? = null,
val requiredDate: LocalDate? // 必填:车道编号
val requiredDate: LocalDate?, // 必填:车道编号
/** When `isExtra`, assignment candidates are limited to `releaseType = isExtra` rows. */
val releaseType: String? = null,
)
data class DoPickOrderSummaryItem(
val truckDepartureTime: java.time.LocalTime?,


+ 5
- 4
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt Zobrazit soubor

@@ -21,7 +21,8 @@ data class ReleaseDoResult(

val truckDepartureTime: LocalTime?,
val truckLanceCode: String?,
val loadingSequence: Int?
val loadingSequence: Int?,
val isExtra: Boolean = false,
)
data class SearchDeliveryOrderInfoRequest(
val code: String?,
@@ -31,8 +32,8 @@ data class SearchDeliveryOrderInfoRequest(
val pageSize: Int?,
val pageNum: Int?,
val truckLanceCode: String?,
/** `ALL`/`All`/null:P06B+P07+P06D;`2F`:P07+P06D;`4F`:P06B。車線-X 亦依供應商歸屬出現在對應樓層。 */
/** `ALL`/`All`/null:P06B+P07+P06D+P06Y;`2F`:P07+P06D+P06Y ;`4F`:P06B。車線-X 亦依供應商歸屬出現在對應樓層。 */
val floor: String? = null,
/** null:不篩 isEtra;true/false:只顯示加單或非加單 DO */
val isEtra: Boolean? = null,
/** null:不篩 isExtra;true/false:只顯示加單或非加單 DO */
val isExtra: Boolean? = null,
)

+ 1
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/SaveDeliveryOrderRequest.kt Zobrazit soubor

@@ -20,7 +20,7 @@ data class SaveDeliveryOrderRequest(
val handlerId: Long?,
val m18BeId: Long?,
val deleted: Boolean? = false,
val isEtra: Boolean? = false,
val isExtra: Boolean? = false,
)

data class SaveDeliveryOrderStatusRequest(


+ 3
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt Zobrazit soubor

@@ -240,6 +240,9 @@ ORDER BY
"id" to row["stockOutLineId"],
"status" to row["stockOutLineStatus"],
"qty" to row["stockOutLineQty"],
"requiredQty" to row["requiredQty"],
"suggestedPickLotQty" to row["requiredQty"],
"suggestedPickLotId" to row["suggestedPickLotId"],
"lotId" to lotId,
"lotNo" to (row["lotNo"] ?: ""),
"location" to (row["location"] ?: ""),


+ 7
- 15
src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt Zobrazit soubor

@@ -10,6 +10,7 @@ import com.ffii.fpsms.m18.M18GrnRules
import com.ffii.fpsms.modules.master.entity.ShopRepository
import com.ffii.fpsms.modules.master.enums.ShopType
import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.deliveryOrder.service.DoFloorSupplierSettingsService
import java.math.BigDecimal
import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter
import net.sf.jasperreports.export.SimpleExporterInput
@@ -20,6 +21,7 @@ open class ReportService(
private val jdbcDao: JdbcDao,
private val itemUomService: ItemUomService,
private val shopRepository: ShopRepository,
private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService,
) {
/**
* Queries the database for inventory data based on dates and optional item type.
@@ -118,6 +120,8 @@ open class ReportService(
"AND DATE(IFNULL(dopo.requiredDeliveryDate, do.estimatedArrivalDate)) <= DATE(:lastOutDateEnd)"
} else ""
val supplierFloorSqlCases = doFloorSupplierSettingsService.sqlPreferredFloorCases("supplier.code")

val sql = """
SELECT
IFNULL(DATE_FORMAT(
@@ -143,17 +147,9 @@ FORMAT(ROUND(IFNULL(IFNULL(sol.qty, dol.qty), 0), 0), 0) AS qty,
FROM truck t2
WHERE t2.shopId = do.shopId
AND t2.deleted = 0
AND t2.Store_id = CASE
WHEN supplier.code = 'P06B' THEN '4F'
WHEN supplier.code IN ('P07', 'P06D') THEN '2F'
ELSE NULL
END
AND t2.Store_id = ${supplierFloorSqlCases.floorStringCase}
AND (
(CASE
WHEN supplier.code = 'P06B' THEN '4F'
WHEN supplier.code IN ('P07', 'P06D') THEN '2F'
ELSE NULL
END
(${supplierFloorSqlCases.floorStringCase}
AND (SELECT COUNT(*) FROM truck t3
WHERE t3.shopId = do.shopId AND t3.deleted = 0
AND t3.Store_id = '4F') > 1
@@ -170,11 +166,7 @@ FORMAT(ROUND(IFNULL(IFNULL(sol.qty, dol.qty), 0), 0), 0) AS qty,
ELSE ''
END, '%'))
OR
t2.Store_id = CASE
WHEN supplier.code = 'P06B' THEN '4F'
WHEN supplier.code IN ('P07', 'P06D') THEN '2F'
ELSE NULL
END
t2.Store_id = ${supplierFloorSqlCases.floorStringCase}
)
ORDER BY t2.DepartureTime ASC
LIMIT 1),


+ 14
- 2
src/main/java/com/ffii/fpsms/modules/settings/web/SettingsController.java Zobrazit soubor

@@ -5,6 +5,7 @@ import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -41,13 +42,24 @@ public class SettingsController{
// @PreAuthorize("hasAuthority('ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void update(@PathVariable String name, @RequestBody @Valid UpdateReq body) {
applyUpdate(name, body);
}

/** Same as PATCH; use from browsers where CORS preflight for PATCH is blocked. */
@PostMapping("/{name}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void updatePost(@PathVariable String name, @RequestBody @Valid UpdateReq body) {
applyUpdate(name, body);
}

private void applyUpdate(String name, UpdateReq body) {
Settings entity = this.settingsService.findByName(name)
.orElseThrow(NotFoundException::new);
if (!this.settingsService.validateType(entity.getType(), body.value)) {
if (!this.settingsService.validateType(entity.getType(), body.getValue())) {
throw new BadRequestException();
}

entity.setValue(body.value);
entity.setValue(body.getValue());
this.settingsService.save(entity);
}



+ 10
- 6
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt Zobrazit soubor

@@ -42,6 +42,7 @@ import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
import com.ffii.fpsms.modules.stock.web.model.StockOutStatus
import com.ffii.fpsms.modules.common.SecurityUtils
import com.ffii.fpsms.modules.deliveryOrder.service.DoFloorSupplierSettingsService
@Service
open class SuggestedPickLotService(
val suggestedPickLotRepository: SuggestPickLotRepository,
@@ -57,7 +58,8 @@ open class SuggestedPickLotService(
val failInventoryLotLineRepository: FailInventoryLotLineRepository,
val stockOutRepository: StockOutRepository,
val itemRepository: ItemsRepository,
val stockOutLineRepository: StockOutLIneRepository
val stockOutLineRepository: StockOutLIneRepository,
private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService,
) {

// Calculation Available Qty / Remaining Qty
@@ -114,6 +116,8 @@ open class SuggestedPickLotService(
.filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today)}
.sortedBy { it.expiryDate }
.groupBy { it.item?.id }

val (floorSuppliers2F, floorSuppliers4F) = doFloorSupplierSettingsService.loadDoFloorSupplierLists()
// loop for suggest pick lot line
pols.forEach { line ->
@@ -126,11 +130,11 @@ open class SuggestedPickLotService(
val doPreferredFloor: String? = if (isDoPickOrder) {
val supplierCode = pickOrder?.deliveryOrder?.supplier?.code
when (supplierCode) {
"P06B" -> "4F"
"P07", "P06D" -> "2F"
else -> null // 其他供应商不限定 2F/4F
}
doFloorSupplierSettingsService.preferredFloorForPickLotOrNull(
supplierCode,
floorSuppliers2F,
floorSuppliers4F,
)
} else {
null
}


+ 8
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt Zobrazit soubor

@@ -0,0 +1,8 @@
package com.ffii.fpsms.modules.stock.web.model

import com.fasterxml.jackson.annotation.JsonIgnoreProperties

@JsonIgnoreProperties(ignoreUnknown = true)
data class CreateStockTakeForSectionsRequest(
val sections: List<String>? = null,
)

+ 18
- 0
src/main/resources/db/changelog/changes/20260514_Enson/01_setting.sql Zobrazit soubor

@@ -0,0 +1,18 @@
--liquibase formatted sql

-- DO 樓層供應商代碼(逗號分隔),name 須與前端 constants 一致。預設值對齊既有硬編碼邏輯,後端改讀 settings 後才會生效。
--changeset Enson:20260514-01
INSERT INTO `fpsmsdb`.`settings` (`name`, `value`, `category`, `type`)
SELECT 'DO.floor.suppliers.2F', 'P07,P06D,P06Y', 'DO_FLOOR', 'string'
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `fpsmsdb`.`settings` WHERE `name` = 'DO.floor.suppliers.2F'
);

--changeset Enson:20260514-02
INSERT INTO `fpsmsdb`.`settings` (`name`, `value`, `category`, `type`)
SELECT 'DO.floor.suppliers.4F', 'P06B', 'DO_FLOOR', 'string'
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `fpsmsdb`.`settings` WHERE `name` = 'DO.floor.suppliers.4F'
);

+ 6
- 0
src/main/resources/db/changelog/changes/20260514_Enson/02_setting.sql Zobrazit soubor

@@ -0,0 +1,6 @@
--liquibase formatted sql

-- 修改 delivery_order 表的 isExtra 欄位為 isExtra
--changeset Enson:20260514-03
ALTER TABLE `delivery_order` CHANGE COLUMN `isEtra` `isExtra` TINYINT(1) NOT NULL DEFAULT 0;


Načítá se…
Zrušit
Uložit