Przeglądaj źródła

bom import, job order stock in line, EPQC

reset-do-picking-order
CANCERYS\kw093 2 tygodni temu
rodzic
commit
42578a3af3
18 zmienionych plików z 1298 dodań i 95 usunięć
  1. +1
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt
  2. +4
    -3
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt
  3. +19
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt
  4. +2
    -2
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt
  5. +3
    -1
      src/main/java/com/ffii/fpsms/modules/master/entity/Bom.kt
  6. +2
    -1
      src/main/java/com/ffii/fpsms/modules/master/entity/BomProcessMaterialRepository.kt
  7. +1
    -1
      src/main/java/com/ffii/fpsms/modules/master/entity/BomRepository.kt
  8. +2
    -2
      src/main/java/com/ffii/fpsms/modules/master/entity/Items.kt
  9. +1081
    -48
      src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt
  10. +52
    -3
      src/main/java/com/ffii/fpsms/modules/master/web/BomController.kt
  11. +39
    -0
      src/main/java/com/ffii/fpsms/modules/master/web/models/ItemUomRequest.kt
  12. +1
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessRepository.kt
  13. +68
    -29
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
  14. +4
    -1
      src/main/java/com/ffii/fpsms/modules/stock/entity/EscalationLogInfo.kt
  15. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt
  16. +4
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
  17. +4
    -0
      src/main/resources/application.yml
  18. +10
    -0
      src/main/resources/db/changelog/changes/20260307_Enson/02_add_isDrink.sql

+ 1
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt Wyświetl plik

@@ -18,7 +18,7 @@ interface JobOrderRepository : AbstractRepository<JobOrder, Long> {
fun findLatestCodeByPrefix(prefix: String): String?

fun findJobOrderInfoByCodeContainsAndBomNameContainsAndDeletedIsFalseOrderByIdDesc(code: String, bomName: String, pageable: Pageable): Page<JobOrderInfo>
fun findByBom_Id(bomId: Long): List<JobOrder>
@Query(
nativeQuery = true,
value = """


+ 4
- 3
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt Wyświetl plik

@@ -1757,7 +1757,7 @@ private fun normalizeFloor(raw: String): String {
val num = cleaned.replace(Regex("[^0-9]"), "")
return if (num.isNotEmpty()) "${num}F" else cleaned
}
open fun getAllJoPickOrders(): List<AllJoPickOrderResponse> {
open fun getAllJoPickOrders(isDrink: Boolean?): List<AllJoPickOrderResponse> {
println("=== getAllJoPickOrders ===")
return try {
@@ -1808,7 +1808,8 @@ open fun getAllJoPickOrders(): List<AllJoPickOrderResponse> {
val bom = jobOrder.bom

// 按 isDrink 过滤:null 表示不过滤(全部)
if (isDrink != null && bom?.isDrink != isDrink) return@mapNotNull null
println("BOM found: ${bom?.id}")
val item = bom?.item
@@ -1860,7 +1861,7 @@ open fun getAllJoPickOrders(): List<AllJoPickOrderResponse> {
}

println("Returning ${jobOrderPickOrders.size} released job order pick orders")
jobOrderPickOrders
jobOrderPickOrders.sortedByDescending { it.id }
} catch (e: Exception) {
println("❌ Error in getAllJoPickOrders: ${e.message}")
e.printStackTrace()


+ 19
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt Wyświetl plik

@@ -22,6 +22,7 @@ import com.ffii.fpsms.modules.pickOrder.service.PickOrderService
import com.ffii.fpsms.modules.pickOrder.web.models.SavePickOrderLineRequest
import com.ffii.fpsms.modules.pickOrder.web.models.SavePickOrderRequest
import com.ffii.fpsms.modules.user.service.UserService

import com.google.gson.reflect.TypeToken
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service
@@ -69,6 +70,8 @@ import java.time.LocalDateTime
import com.ffii.fpsms.modules.master.entity.BomMaterialRepository
import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.master.web.models.ConvertUomByItemRequest
import com.ffii.fpsms.modules.stock.service.StockInLineService
import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest
@Service
open class JobOrderService(
val jobOrderRepository: JobOrderRepository,
@@ -87,6 +90,7 @@ open class JobOrderService(
val jobTypeRepository: JobTypeRepository,
val inventoryRepository: InventoryRepository,
val stockInLineRepository: StockInLineRepository,
val stockInLineService: StockInLineService,
val productProcessRepository: ProductProcessRepository,
val jobOrderBomMaterialRepository: JobOrderBomMaterialRepository,
val bomMaterialRepository: BomMaterialRepository,
@@ -437,7 +441,21 @@ open class JobOrderService(
}

val savedJo = jobOrderRepository.saveAndFlush(jo);

savedJo.bom?.item?.id?.let { fgItemId ->
stockInLineService.create(
SaveStockInLineRequest(
itemId = fgItemId,
acceptedQty = BigDecimal.ZERO,
acceptQty = null,
jobOrderId = savedJo.id,
status = "pending",
expiryDate = null,
productLotNo = null,
productionDate = null,
receiptDate = null,
)
)
}
return MessageResponse(
id = savedJo.id,
name = null,


+ 2
- 2
src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt Wyświetl plik

@@ -243,8 +243,8 @@ fun recordSecondScanIssue(
return joPickOrderService.getCompletedJobOrderPickOrderLotDetailsForCompletedPick(pickOrderId)
}
@GetMapping("/AllJoPickOrder")
fun getAllJoPickOrder(): List<AllJoPickOrderResponse> {
return joPickOrderService.getAllJoPickOrders()
fun getAllJoPickOrder(@RequestParam(required = false) isDrink: Boolean?): List<AllJoPickOrderResponse> {
return joPickOrderService.getAllJoPickOrders(isDrink)
}

@GetMapping("/all-lots-hierarchical-by-pick-order/{pickOrderId}")


+ 3
- 1
src/main/java/com/ffii/fpsms/modules/master/entity/Bom.kt Wyświetl plik

@@ -22,6 +22,8 @@ open class Bom : BaseEntity<Long>() {
@Column
open var isDense: Int? = null
@Column
open var isDrink: Boolean? = false
@Column
open var scrapRate: Int? = null
@Column
open var allergicSubstances: Int? = null
@@ -31,7 +33,7 @@ open class Bom : BaseEntity<Long>() {
@Column
open var complexity: Int? = null
@JsonBackReference
@OneToOne
@ManyToOne
@JoinColumn(name = "itemId")
open var item: Items? = null



+ 2
- 1
src/main/java/com/ffii/fpsms/modules/master/entity/BomProcessMaterialRepository.kt Wyświetl plik

@@ -5,5 +5,6 @@ import org.springframework.stereotype.Repository

@Repository
interface BomProcessMaterialRepository : AbstractRepository<BomProcessMaterial, Long> {
fun findByBomProcessIdAndBomMaterialId(bomProcessId: Long, bomMaterialId: Long): BomProcessMaterial?;
fun findByBomProcessIdAndBomMaterialId(bomProcessId: Long, bomMaterialId: Long): BomProcessMaterial?
fun findByBomProcess_IdIn(bomProcessIds: List<Long>): List<BomProcessMaterial>
}

+ 1
- 1
src/main/java/com/ffii/fpsms/modules/master/entity/BomRepository.kt Wyświetl plik

@@ -16,6 +16,6 @@ interface BomRepository : AbstractRepository<Bom, Long> {
fun findByItemIdAndDeletedIsFalse(itemId: Serializable): Bom?

fun findBomComboByDeletedIsFalse(): List<BomCombo>
fun findAllByItemIdAndDeletedIsFalse(itemId: Long): List<Bom>
fun findByCodeAndDeletedIsFalse(code: String): Bom?
}

+ 2
- 2
src/main/java/com/ffii/fpsms/modules/master/entity/Items.kt Wyświetl plik

@@ -76,8 +76,8 @@ open class Items : BaseEntity<Long>() {
open var inventories: MutableList<Inventory> = mutableListOf()

@JsonManagedReference
@OneToOne(mappedBy = "item", cascade = [CascadeType.ALL], orphanRemoval = true)
open var bom: Bom? = null
@OneToMany(mappedBy = "item", cascade = [CascadeType.ALL], orphanRemoval = true)
open var boms: MutableList<Bom> = mutableListOf()

@ManyToOne
@JoinColumn(name = "categoryId")


+ 1081
- 48
src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt
Plik diff jest za duży
Wyświetl plik


+ 52
- 3
src/main/java/com/ffii/fpsms/modules/master/web/BomController.kt Wyświetl plik

@@ -11,15 +11,64 @@ import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
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
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.multipart.MultipartHttpServletRequest
import org.slf4j.LoggerFactory
import java.time.LocalDate

import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.Part
import com.ffii.fpsms.modules.master.web.models.BomFormatCheckResponse
import com.ffii.fpsms.modules.master.web.models.BomUploadResponse
import com.ffii.fpsms.modules.master.web.models.BomFormatCheckRequest
import com.ffii.fpsms.modules.master.web.models.ImportBomRequestPayload
import java.util.logging.Logger
@RestController
@RequestMapping("/bom")
class BomController (
val bomService: BomService, private val bomRepository: BomRepository,
) {
private val log = LoggerFactory.getLogger(BomController::class.java)

@PostMapping("/import-bom/upload")
fun uploadBomFiles(request: HttpServletRequest): BomUploadResponse {

val multipartRequest = request as? MultipartHttpServletRequest
if (multipartRequest != null) {
val files = multipartRequest.getFiles("files").toList().filterNot { it.isEmpty }
if (files.isNotEmpty()) {
log.info("import-bom/upload: using getFiles(\"files\"), count={}", files.size)
return bomService.uploadBomFiles(files)
}
// 先嘗試 Spring 包裝的 MultipartHttpServletRequest(與其他 Controller 一致)
log.info("import-bom/upload: received files count={}", files.size)
files.forEachIndexed { i, f ->
log.info("import-bom/upload: file[{}] originalFilename={}", i, f.originalFilename)
}
}
// 否則用 Servlet getParts()(Postman 等可能不會被包裝成 MultipartHttpServletRequest)
val allParts = try {
request.parts
} catch (e: Exception) {
log.info("import-bom/upload: request.parts failed, content-type may not be multipart: {}", e.message)
return BomUploadResponse(batchId = "", fileNames = emptyList())
}
val parts = allParts.filter { it.name == "files" }
if (parts.isEmpty()) {
log.info("import-bom/upload: no parts with name \"files\". All part names: {}", allParts.map { it.name })
return BomUploadResponse(batchId = "", fileNames = emptyList())
}
log.info("import-bom/upload: using getParts(), count={}", parts.size)
return bomService.uploadBomFilesFromParts(parts)
}

@PostMapping("/import-bom/format-check")
fun checkBomFormat(@RequestBody request: BomFormatCheckRequest): BomFormatCheckResponse {
return bomService.checkBomExcelFormat(request.batchId)
}
@GetMapping
fun getBoms(): List<Bom> {
return bomService.findAll()
@@ -31,8 +80,8 @@ class BomController (
}

@PostMapping("/import-bom")
fun importBom(): ResponseEntity<Resource> {
val reportResult = bomService.importBOM()
fun importBom(@RequestBody payload: ImportBomRequestPayload): ResponseEntity<Resource> {
val reportResult = bomService.importBOM(payload.batchId, payload.items)
val filename = "bom_excel_issue_log_${LocalDate.now()}.xlsx"

return ResponseEntity.ok()


+ 39
- 0
src/main/java/com/ffii/fpsms/modules/master/web/models/ItemUomRequest.kt Wyświetl plik

@@ -27,4 +27,43 @@ data class ItemUomRequest(
val ratioD: BigDecimal?,
val ratioN: BigDecimal?,
val deleted: Boolean?,
)
data class BomFormatIssue(
val fileName: String, // 檔名
val problem: String // 問題描述(用來 group 的 key)
)

// 回傳給前端用:問題 → 檔名列表
data class BomFormatProblemGroup(
val problem: String,
val fileNames: List<String>
)
data class BomFormatFileGroup(
val fileName: String,
val problems: List<String>
)

data class BomUploadResponse(
val batchId: String,
val fileNames: List<String>
)

data class BomFormatCheckRequest(
val batchId: String
)

/** Format-check 回傳:正確檔名列表 + 失敗列表(檔名 + 問題) */
data class BomFormatCheckResponse(
val correctFileNames: List<String>,
val failList: List<BomFormatFileGroup>
)

data class ImportBomItemRequest(
val fileName: String,
val isAlsoWip: Boolean = false
)

data class ImportBomRequestPayload(
val batchId: String,
val items: List<ImportBomItemRequest>
)

+ 1
- 1
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessRepository.kt Wyświetl plik

@@ -12,5 +12,5 @@ interface ProductProcessRepository : JpaRepository<ProductProcess, Long>, JpaSpe
fun findByProductProcessCodeStartingWith(prefix: String): List<ProductProcess>
fun findByIdAndDeletedIsFalse(id: Long): Optional<ProductProcess>
fun findAllByDeletedIsFalse(): List<ProductProcess>
fun findByBom_Id(bomId: Long): List<ProductProcess>
}

+ 68
- 29
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt Wyświetl plik

@@ -52,7 +52,7 @@ import com.ffii.fpsms.modules.master.web.models.*
import java.math.RoundingMode
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
@Service
@Transactional
open class ProductProcessService(
@@ -78,7 +78,8 @@ open class ProductProcessService(
private val joPickOrderRepository: JoPickOrderRepository,
private val itemUomRepository: ItemUomRespository,
private val uomConversionRepository: UomConversionRepository,
private val itemUomService: ItemUomService
private val itemUomService: ItemUomService,
private val stockInLineRepository: StockInLineRepository
) {

open fun findAll(pageable: Pageable): Page<ProductProcess> {
@@ -1446,10 +1447,13 @@ open class ProductProcessService(
val jobOrder = jobOrderRepository.findById(response.jobOrderId ?: 0L).orElse(null)
val stockInLineStatus = jobOrder?.stockInLines?.firstOrNull()?.status
stockInLineStatus != "completed"
&& stockInLineStatus != "escalated"
// && stockInLineStatus != "escalated" // 你已經註解掉,會把 escalated 顯示出來
&& stockInLineStatus != "rejected"
&& jobOrder?.status != JobOrderStatus.PLANNING
}
}.sortedWith(
compareByDescending<AllJoborderProductProcessInfoResponse> { it.date ?: LocalDate.MIN }
.thenBy { it.productionPriority ?: Int.MAX_VALUE }
)
}

open fun updateProductProcessLineStartTime(productProcessLineId: Long): MessageResponse {
@@ -1567,20 +1571,36 @@ open class ProductProcessService(
jobOrder.status = JobOrderStatus.STORING
jobOrderRepository.save(jobOrder)

stockInLineService.create(
SaveStockInLineRequest(
itemId = productProcess?.item?.id ?: 0L,
acceptedQty = jobOrder?.reqQty ?: BigDecimal.ZERO,
productLotNo = jobOrder?.code,
productionDate = LocalDate.now(),
jobOrderId = jobOrder.id,
acceptQty = jobOrder?.reqQty ?: BigDecimal.ZERO,
//acceptQty = null,
expiryDate = null,
status = "pending",
val existingSil = jobOrder.id?.let { stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(it) }
if (existingSil != null) {
stockInLineService.update(
SaveStockInLineRequest(
id = existingSil.id,
itemId = existingSil.item?.id ?: productProcess?.item?.id ?: 0L,
acceptedQty = jobOrder.reqQty ?: BigDecimal.ZERO,
acceptQty = jobOrder.reqQty ?: BigDecimal.ZERO,
productLotNo = jobOrder.code,
productionDate = LocalDate.now(),
jobOrderId = jobOrder.id,
expiryDate = null,
status = "pending",
receiptDate = null,
)
)
)

} else {
stockInLineService.create(
SaveStockInLineRequest(
itemId = productProcess?.item?.id ?: 0L,
acceptedQty = jobOrder?.reqQty ?: BigDecimal.ZERO,
productLotNo = jobOrder?.code,
productionDate = LocalDate.now(),
jobOrderId = jobOrder.id,
acceptQty = jobOrder?.reqQty ?: BigDecimal.ZERO,
expiryDate = null,
status = "pending",
)
)
}
}
}
return MessageResponse(
@@ -1609,19 +1629,38 @@ open class ProductProcessService(
if (jobOrder != null) {
jobOrder.status = JobOrderStatus.STORING
jobOrderRepository.save(jobOrder)

stockInLineService.create(
SaveStockInLineRequest(
itemId = productProcess?.item?.id ?: 0L,
acceptedQty = jobOrder?.reqQty ?: BigDecimal.ZERO,
productLotNo = jobOrder?.code,
productionDate = LocalDate.now(),
jobOrderId = jobOrder.id,
acceptQty = jobOrder?.reqQty ?: BigDecimal.ZERO,
expiryDate = null,
status = "",
val existingSil = jobOrder.id?.let { stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(it) }
if (existingSil != null) {
stockInLineService.update(
SaveStockInLineRequest(
id = existingSil.id,
itemId = existingSil.item?.id ?: productProcess?.item?.id ?: 0L,
acceptedQty = jobOrder.reqQty ?: BigDecimal.ZERO,
acceptQty = jobOrder.reqQty ?: BigDecimal.ZERO,
productLotNo = jobOrder.code,
productionDate = LocalDate.now(),
jobOrderId = jobOrder.id,
expiryDate = null,
status = "pending",
receiptDate = null,
)
)
)
} else {
stockInLineService.create(
SaveStockInLineRequest(
itemId = productProcess?.item?.id ?: 0L,
acceptedQty = jobOrder?.reqQty ?: BigDecimal.ZERO,
productLotNo = jobOrder?.code,
productionDate = LocalDate.now(),
jobOrderId = jobOrder.id,
acceptQty = jobOrder?.reqQty ?: BigDecimal.ZERO,
expiryDate = null,
status = "",
)
)
}
}
}



+ 4
- 1
src/main/java/com/ffii/fpsms/modules/stock/entity/EscalationLogInfo.kt Wyświetl plik

@@ -12,7 +12,10 @@ interface EscalationLogInfo {

@get:Value("#{target.stockInLine?.purchaseOrderLine?.id}")
val polId: Long?

@get:Value("#{target.stockInLine?.jobOrder?.id}")
val jobOrderId: Long?
@get:Value("#{target.stockInLine?.jobOrder?.code}")
val jobOrderCode: String?
@get:Value("#{target.stockInLine?.stockIn?.code}")
val poCode: String?



+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt Wyświetl plik

@@ -55,4 +55,5 @@ fun searchStockInLines(
//AND sil.type IS NOT NULL
@Query("SELECT sil FROM StockInLine sil WHERE sil.item.id IN :itemIds AND sil.deleted = false")
fun findAllByItemIdInAndDeletedFalse(itemIds: List<Long>): List<StockInLine>
fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine?
}

+ 4
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Wyświetl plik

@@ -210,6 +210,9 @@ open class StockInLineService(
this.type = "Nor"
status = StockInLineStatus.PENDING.status
}
if (jo != null) {
stockInLine.lotNo = assignLotNo()
}
val savedSIL = saveAndFlush(stockInLine)
if (pol != null) {
//logger.info("[create] Stock-in line created with PO, running PO/GRN update for stockInLine id=${savedSIL.id}")
@@ -238,7 +241,7 @@ open class StockInLineService(
)
val inventoryCount = jdbcDao.queryForInt(INVENTORY_COUNT.toString(), args)
// val newLotNo = CodeGenerator.generateCode(prefix = "MPO", itemId = stockInLine.item!!.id!!, count = inventoryCount)
val newLotNo = assignLotNo()
val newLotNo = stockInLine.lotNo ?: assignLotNo()
inventoryLot.apply {
this.item = stockInLine.item
this.stockInLine = stockInLine


+ 4
- 0
src/main/resources/application.yml Wyświetl plik

@@ -27,6 +27,10 @@ spring:
logging:
config: 'classpath:log4j2.yml'

bom:
import:
temp-dir: ${java.io.tmpdir}/fpsms-bom-import

m18:
config:
grant-type: password


+ 10
- 0
src/main/resources/db/changelog/changes/20260307_Enson/02_add_isDrink.sql Wyświetl plik

@@ -0,0 +1,10 @@
-- liquibase formatted sql
-- changeset Enson:add_isDrink_to_bom



ALTER TABLE `fpsmsdb`.`bom`
ADD COLUMN `isDrink` TINYINT(1) NULL DEFAULT false AFTER `isDense`;



Ładowanie…
Anuluj
Zapisz