Tommy\2Fi-Staff 3 tygodni temu
rodzic
commit
e02f7cc191
33 zmienionych plików z 893 dodań i 165 usunięć
  1. +2
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  2. +1
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt
  3. +95
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt
  4. +1
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt
  5. +5
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt
  6. +17
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/SearchJobOrderInfoRequest.kt
  7. +1
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt
  8. +1
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderLineRepository.kt
  9. +2
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  10. +41
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
  11. +4
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt
  12. +15
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt
  13. +8
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt
  14. +24
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt
  15. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockInRepository.kt
  16. +12
    -4
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedger.kt
  17. +44
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedgerRepository.kt
  18. +2
    -1
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockOut.kt
  19. +26
    -1
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt
  20. +4
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLine.kt
  21. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutRepository.kt
  22. +22
    -11
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt
  23. +87
    -2
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
  24. +56
    -13
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  25. +297
    -120
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  26. +2
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt
  27. +45
    -10
      src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt
  28. +16
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockInRequest.kt
  29. +32
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt
  30. +7
    -0
      src/main/resources/db/changelog/changes/20260109_enson/02_alter_table.sql
  31. +8
    -0
      src/main/resources/db/changelog/changes/20260112_01_Enson/01_alter_table.sql
  32. +7
    -0
      src/main/resources/db/changelog/changes/20260112_01_Enson/02_alter_table.sql
  33. +7
    -0
      src/main/resources/db/changelog/changes/20260112_01_Enson/03_alter_table.sql

+ 2
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt Wyświetl plik

@@ -510,6 +510,7 @@ open class DeliveryOrderService(
this.item = pickOrderLine.item this.item = pickOrderLine.item
this.status = StockOutLineStatus.PENDING.status this.status = StockOutLineStatus.PENDING.status
this.qty = 0.0 this.qty = 0.0
this.type = "Nor"
} }
stockOutLineRepository.save(line) stockOutLineRepository.save(line)
} }
@@ -1178,6 +1179,7 @@ open class DeliveryOrderService(
StockOutLineStatus.PENDING.status // 有正常库存批次时使用 PENDING StockOutLineStatus.PENDING.status // 有正常库存批次时使用 PENDING
} }
this.qty = 0.0 this.qty = 0.0
this.type = "Nor"
} }
stockOutLineRepository.save(line) stockOutLineRepository.save(line)
} }


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt Wyświetl plik

@@ -96,7 +96,7 @@ class DoPickOrderController(
return doReleaseCoordinatorService.getBatchReleaseProgress(jobId) return doReleaseCoordinatorService.getBatchReleaseProgress(jobId)
} }


@GetMapping("/ticket-release-table/{startDate}/{endDate}")
@GetMapping("/ticket-release-table/{startDate}&{endDate}")
fun getTicketReleaseTable( fun getTicketReleaseTable(
@PathVariable startDate: LocalDate, @PathVariable startDate: LocalDate,
@PathVariable endDate: LocalDate @PathVariable endDate: LocalDate


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

@@ -49,6 +49,7 @@ import com.ffii.fpsms.modules.jobOrder.web.model.LotDetailResponse
import com.ffii.fpsms.modules.stock.entity.enum.StockInLineStatus import com.ffii.fpsms.modules.stock.entity.enum.StockInLineStatus
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import java.time.LocalDate import java.time.LocalDate
import com.ffii.fpsms.modules.jobOrder.web.model.MaterialPickStatusItem
@Service @Service
open class JoPickOrderService( open class JoPickOrderService(
private val joPickOrderRepository: JoPickOrderRepository, private val joPickOrderRepository: JoPickOrderRepository,
@@ -2249,5 +2250,98 @@ open fun deleteJoPickOrderJobOrderProductProcessPickOrder(jobOrderId: Long): Mes
errorPosition = null errorPosition = null
) )
} }

open fun getMaterialPickStatus(): List<MaterialPickStatusItem> {
val joPickOrders = joPickOrderRepository.findAll()
return joPickOrders
.filter { joPickOrder ->
val jobOrder = joPickOrder.jobOrderId?.let {
jobOrderRepository.findById(it).orElse(null)
}
jobOrder?.status != JobOrderStatus.COMPLETED
}
.map { joPickOrder ->
// Get related entities for additional data
val jobOrder = joPickOrder.jobOrderId?.let {
jobOrderRepository.findById(it).orElse(null)
}
val item = joPickOrder.itemId?.let {
itemsRepository.findById(it).orElse(null)
}
val pickOrder = joPickOrder.pickOrderId?.let {
pickOrderRepository.findById(it).orElse(null)
}
// Get stock out lines for this pick order and item to get start/end times
// First get all pick order lines for this pick order
val pickOrderLines = pickOrder?.let { po ->
pickOrderLineRepository.findByPickOrderId(po.id!!)
} ?: emptyList()
// Then get stock out lines for each pick order line that matches the item
val stockOutLines = pickOrderLines
.filter { it.item?.id == joPickOrder.itemId }
.flatMap { pol ->
stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(pol.id!!)
.mapNotNull { solInfo ->
// Convert StockOutLineInfo to StockOutLine entity to access startTime/endTime
// We need to fetch the actual entity
stockOutLineRepository.findById(solInfo.id).orElse(null)
}
}
// Get earliest startTime and latest endTime from stock out lines
val pickStartTime = stockOutLines
.mapNotNull { it.startTime }
.minOrNull()
val pickEndTime = stockOutLines
.mapNotNull { it.endTime }
.maxOrNull()
// Count items to pick from pick order lines
val numberOfItemsToPick = pickOrder?.let { po ->
pickOrderLineRepository.findByPickOrderId(po.id!!).size
} ?: 0
// Count items with issues from pick execution issues
val numberOfItemsWithIssue = joPickOrder.id?.let { joPickOrderId ->
pickExecutionIssueRepository.findByPickOrderIdAndDeletedFalse(joPickOrder.pickOrderId ?: 0L)
.filter { it.joPickOrderId == joPickOrderId }
.size
} ?: 0
// Get job order quantity and UOM - access via bom
val jobOrderQty = jobOrder?.reqQty?.toDouble()
val uom = jobOrder?.bom?.uom?.code
// Get item name
val itemName = item?.name
// Determine pick status - prioritize pickOrder.status over matchStatus
val pickStatus = when {
pickOrder?.status != null -> pickOrder.status!!.value
joPickOrder.matchStatus != null -> joPickOrder.matchStatus!!.value
else -> null
}
MaterialPickStatusItem(
id = joPickOrder.id ?: 0L,
pickOrderId = joPickOrder.pickOrderId,
pickOrderCode = joPickOrder.pickOrderCode,
jobOrderId = joPickOrder.jobOrderId,
jobOrderCode = joPickOrder.jobOrderCode,
itemId = joPickOrder.itemId,
itemCode = joPickOrder.itemCode,
itemName = itemName,
jobOrderQty = jobOrderQty,
uom = uom,
pickStartTime = pickStartTime,
pickEndTime = pickEndTime,
numberOfItemsToPick = numberOfItemsToPick,
numberOfItemsWithIssue = numberOfItemsWithIssue,
pickStatus = pickStatus
)
}
} }
}

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

@@ -508,6 +508,7 @@ open class JobOrderService(
this.item = pickOrderLine.item this.item = pickOrderLine.item
this.status = StockOutLineStatus.PENDING.status this.status = StockOutLineStatus.PENDING.status
this.qty = 0.0 this.qty = 0.0
this.type = "Nor"
} }
stockOutLineRepository.save(line) stockOutLineRepository.save(line)
} }


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

@@ -40,6 +40,7 @@ import com.ffii.fpsms.modules.jobOrder.web.model.UpdateJoPickOrderHandledByReque
import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderInfo import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderInfo
import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderInfoWithTypeName import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderInfoWithTypeName
import com.ffii.fpsms.modules.jobOrder.web.model.ExportFGStockInLabelRequest import com.ffii.fpsms.modules.jobOrder.web.model.ExportFGStockInLabelRequest
import com.ffii.fpsms.modules.jobOrder.web.model.MaterialPickStatusItem
import java.time.LocalDate import java.time.LocalDate


@RestController @RestController
@@ -320,4 +321,8 @@ fun checkJobOrderCreated(@Valid @RequestBody request: CheckJobOrderCreatedReques
fun updateJoReqQty(@Valid @RequestBody request: UpdateJoReqQtyRequest): MessageResponse { fun updateJoReqQty(@Valid @RequestBody request: UpdateJoReqQtyRequest): MessageResponse {
return jobOrderService.updateJoReqQty(request) return jobOrderService.updateJoReqQty(request)
} }
@GetMapping("/material-pick-status")
fun getMaterialPickStatus(): List<MaterialPickStatusItem> {
return joPickOrderService.getMaterialPickStatus()
}
} }

+ 17
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/SearchJobOrderInfoRequest.kt Wyświetl plik

@@ -11,3 +11,20 @@ data class SearchJobOrderInfoRequest(
val pageNum: Int?, val pageNum: Int?,
val jobTypeName: String?, val jobTypeName: String?,
) )
data class MaterialPickStatusItem(
val id: Long,
val pickOrderId: Long?,
val pickOrderCode: String?,
val jobOrderId: Long?,
val jobOrderCode: String?,
val itemId: Long?,
val itemCode: String?,
val itemName: String?,
val jobOrderQty: Double?,
val uom: String?,
val pickStartTime: LocalDateTime?,
val pickEndTime: LocalDateTime?,
val numberOfItemsToPick: Int,
val numberOfItemsWithIssue: Int,
val pickStatus: String?
)

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

@@ -74,6 +74,7 @@ fun findMaterialIssues(): List<PickExecutionIssue>
ORDER BY p.created DESC ORDER BY p.created DESC
""") """)
fun getBadItemList(): List<PickExecutionIssue> fun getBadItemList(): List<PickExecutionIssue>
fun findByPickOrderId(pickOrderId: Long): List<PickExecutionIssue>


} }




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

@@ -22,4 +22,5 @@ fun findAllPickOrdersByItemId(@Param("itemId") itemId: Long): List<PickOrder>
""") """)
fun findAllByPickOrderId(@Param("pickOrderId") pickOrderId: Long): List<PickOrderLine> fun findAllByPickOrderId(@Param("pickOrderId") pickOrderId: Long): List<PickOrderLine>
fun findAllByPickOrderIdAndDeletedFalse(pickOrderId: Long): List<PickOrderLine> fun findAllByPickOrderIdAndDeletedFalse(pickOrderId: Long): List<PickOrderLine>
fun findByPickOrderId(pickOrderId: Long): List<PickOrderLine>
} }

+ 2
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt Wyświetl plik

@@ -1147,6 +1147,7 @@ open class PickOrderService(
this.status = this.status =
com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus.PENDING.status com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus.PENDING.status
this.qty = 0.0 this.qty = 0.0
this.type = "Nor"
} }
stockOutLIneRepository.save(line) stockOutLIneRepository.save(line)
precreated++ precreated++
@@ -1945,6 +1946,7 @@ open class PickOrderService(
this.status = this.status =
com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus.PENDING.status com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus.PENDING.status
this.qty = 0.0 this.qty = 0.0
this.type = "Nor"
} }
stockOutLIneRepository.save(line) stockOutLIneRepository.save(line)
precreated++ precreated++


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

@@ -1587,5 +1587,45 @@ open class ProductProcessService(
message = "ProductProcess Line Created successfully with seqNo ${originalSeqNo + 1}", message = "ProductProcess Line Created successfully with seqNo ${originalSeqNo + 1}",
errorPosition = null, errorPosition = null,
) )
}}
}

open fun getJobProcessStatus(): List<JobProcessStatusResponse> {
val productProcesses = productProcessRepository.findAllByDeletedIsFalse()
.filter { it.status != ProductProcessStatus.COMPLETED }
return productProcesses.map { process ->
val jobOrder = jobOrderRepository.findById(process.jobOrder?.id ?: 0L).orElse(null)
val lines = productProcessLineRepository.findByProductProcess_Id(process.id ?: 0L)
.sortedBy { it.seqNo }
val bom=bomRepository.findById(process.bom?.id ?: 0L).orElse(null)
val bomProcesses = bomProcessRepository.findByBomId(bom?.id ?: 0L).sortedBy { it.seqNo }
JobProcessStatusResponse(
jobOrderId = jobOrder?.id ?: 0L,
jobOrderCode = jobOrder?.code ?: "",
itemCode = process.item?.code ?: "",
itemName = process.item?.name ?: "",
planEndTime = jobOrder?.planEnd,
processes = (0 until 6).map { index ->
if (index < lines.size) {
val line = lines[index]
ProcessStatusInfo(
equipmentCode = bomProcesses[index].equipment?.code ?: "",
startTime = line.startTime,
endTime = line.endTime,
isRequired = true
)
} else {
ProcessStatusInfo(
startTime = null,
endTime = null,
isRequired = false,
equipmentCode = null,
)
}
}
)
}
}

}



+ 4
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt Wyświetl plik

@@ -225,4 +225,8 @@ class ProductProcessController(
fun updateProductProcessLineProcessingTimeSetupTimeChangeoverTime(@PathVariable lineId: Long, @RequestBody request: UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest): MessageResponse { fun updateProductProcessLineProcessingTimeSetupTimeChangeoverTime(@PathVariable lineId: Long, @RequestBody request: UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest): MessageResponse {
return productProcessService.UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTime(request) return productProcessService.UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTime(request)
} }
@GetMapping("/Demo/JobProcessStatus")
fun getJobProcessStatus(): List<JobProcessStatusResponse> {
return productProcessService.getJobProcessStatus()
}
} }

+ 15
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt Wyświetl plik

@@ -218,4 +218,19 @@ data class UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest(
val processingTime: Int?, val processingTime: Int?,
val setupTime: Int?, val setupTime: Int?,
val changeoverTime: Int? val changeoverTime: Int?
)
data class ProcessStatusInfo(
val equipmentCode: String?,
val startTime: LocalDateTime?,
val endTime: LocalDateTime?,
val isRequired: Boolean
)

data class JobProcessStatusResponse(
val jobOrderId: Long,
val jobOrderCode: String,
val itemCode: String,
val itemName: String,
val planEndTime: LocalDateTime?,
val processes: List<ProcessStatusInfo>
) )

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

@@ -70,4 +70,12 @@ fun findAllByWarehouseCodeAndDeletedIsFalse(@Param("warehouseCode") warehouseCod
AND ill.deleted = false AND ill.deleted = false
""") """)
fun countDistinctItemsByWarehouseIds(@Param("warehouseIds") warehouseIds: List<Long>): Long fun countDistinctItemsByWarehouseIds(@Param("warehouseIds") warehouseIds: List<Long>): Long

@Query("""
SELECT COUNT(ill.id)
FROM InventoryLotLine ill
WHERE ill.warehouse.id IN :warehouseIds
AND ill.deleted = false
""")
fun countAllByWarehouseIds(@Param("warehouseIds") warehouseIds: List<Long>): Long
} }

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

@@ -7,6 +7,9 @@ import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.util.Optional import java.util.Optional
import java.time.LocalDate import java.time.LocalDate
import org.springframework.data.repository.query.Param


@Repository @Repository
interface StockInLineRepository : AbstractRepository<StockInLine, Long> { interface StockInLineRepository : AbstractRepository<StockInLine, Long> {
fun findAllStockInLineInfoByStockInIdAndDeletedFalse(stockInId: Long): List<StockInLineInfo> fun findAllStockInLineInfoByStockInIdAndDeletedFalse(stockInId: Long): List<StockInLineInfo>
@@ -31,4 +34,25 @@ interface StockInLineRepository : AbstractRepository<StockInLine, Long> {
AND sil.deleted = false AND sil.deleted = false
""") """)
fun findByReceiptDateAndDeletedFalse(date: LocalDate): List<StockInLine> fun findByReceiptDateAndDeletedFalse(date: LocalDate): List<StockInLine>
@Query("""
SELECT sil FROM StockInLine sil
WHERE sil.deleted = false
AND (:type IS NULL OR sil.type IS NOT NULL)
AND (:startDate IS NULL OR DATE(sil.created) >= :startDate)
AND (:endDate IS NULL OR DATE(sil.created) <= :endDate)
AND (:itemCode IS NULL OR sil.item.code LIKE CONCAT('%', :itemCode, '%'))
AND (:itemName IS NULL OR sil.item.name LIKE CONCAT('%', :itemName, '%'))
AND (:type IS NULL OR sil.type = :type)
ORDER BY sil.created DESC
""")
fun searchStockInLines(
@Param("startDate") startDate: LocalDate?,
@Param("endDate") endDate: LocalDate?,
@Param("itemCode") itemCode: String?,
@Param("itemName") itemName: String?,
@Param("type") type: String?
): List<StockInLine>
//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>
} }

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

@@ -21,4 +21,5 @@ interface StockInRepository : AbstractRepository<StockIn, Long> {
select si.code from StockIn si where si.code like :prefix% and si.deleted = false order by si.code desc limit 1 select si.code from StockIn si where si.code like :prefix% and si.deleted = false order by si.code desc limit 1
""") """)
fun findLatestCodeByPrefix(prefix: String): String? fun findLatestCodeByPrefix(prefix: String): String?

} }

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

@@ -7,13 +7,13 @@ import jakarta.persistence.JoinColumn
import jakarta.persistence.OneToOne import jakarta.persistence.OneToOne
import jakarta.persistence.Table import jakarta.persistence.Table
import jakarta.validation.constraints.NotNull import jakarta.validation.constraints.NotNull
import java.time.LocalDate
@Entity @Entity
@Table(name = "stock_ledger") @Table(name = "stock_ledger")
open class StockLedger: BaseEntity<Long>() { open class StockLedger: BaseEntity<Long>() {
// @OneToOne
// @JoinColumn(name = "stockInLineId")
// open var stockInLine: StockInLine? = null
@OneToOne
@JoinColumn(name = "stockInLineId")
open var stockInLine: StockInLine? = null


@OneToOne @OneToOne
@JoinColumn(name = "stockOutLineId") @JoinColumn(name = "stockOutLineId")
@@ -32,4 +32,12 @@ open class StockLedger: BaseEntity<Long>() {


@Column(name = "balance") @Column(name = "balance")
open var balance: Double? = null open var balance: Double? = null
@Column(name = "type")
open var type: String? = null
@Column(name = "itemId")
open var itemId: Long? = null
@Column(name = "itemCode")
open var itemCode: String? = null
@Column(name = "date")
open var date: LocalDate? = null
} }

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

@@ -1,8 +1,52 @@
package com.ffii.fpsms.modules.stock.entity package com.ffii.fpsms.modules.stock.entity


import com.ffii.core.support.AbstractRepository import com.ffii.core.support.AbstractRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.time.LocalDate


@Repository @Repository
interface StockLedgerRepository: AbstractRepository<StockLedger, Long> { interface StockLedgerRepository: AbstractRepository<StockLedger, Long> {
@Query("""
SELECT sl FROM StockLedger sl
LEFT JOIN sl.stockInLine sil
LEFT JOIN sl.stockOutLine sol
LEFT JOIN sl.inventory inv
LEFT JOIN inv.item i
WHERE sl.deleted = false
AND (:itemCode IS NULL OR sl.itemCode LIKE CONCAT('%', :itemCode, '%'))
AND (:itemName IS NULL OR i.name LIKE CONCAT('%', :itemName, '%'))
AND (:type IS NULL OR sl.type = :type)
AND (:startDate IS NULL OR DATE(sl.created) >= :startDate)
AND (:endDate IS NULL OR DATE(sl.created) <= :endDate)
ORDER BY sl.created ASC, sl.itemId
""")
fun findStockTransactions(
@Param("itemCode") itemCode: String?,
@Param("itemName") itemName: String?,
@Param("type") type: String?,
@Param("startDate") startDate: LocalDate?,
@Param("endDate") endDate: LocalDate?
): List<StockLedger>
@Query("""
SELECT COUNT(sl) FROM StockLedger sl
LEFT JOIN sl.inventory inv
LEFT JOIN inv.item i
WHERE sl.deleted = false
AND (:itemCode IS NULL OR sl.itemCode LIKE CONCAT('%', :itemCode, '%'))
AND (:itemName IS NULL OR i.name LIKE CONCAT('%', :itemName, '%'))
AND (:type IS NULL OR sl.type = :type)
AND (:startDate IS NULL OR DATE(sl.created) >= :startDate)
AND (:endDate IS NULL OR DATE(sl.created) <= :endDate)
""")
fun countStockTransactions(
@Param("itemCode") itemCode: String?,
@Param("itemName") itemName: String?,
@Param("type") type: String?,
@Param("startDate") startDate: LocalDate?,
@Param("endDate") endDate: LocalDate?
): Long
} }

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

@@ -34,5 +34,6 @@ open class StockOut: BaseEntity<Long>(){


@Column(name = "remarks") @Column(name = "remarks")
open var remarks: String? = null open var remarks: String? = null

@Column(name = "stockTakeId")
open var stockTakeId: Long? = null
} }

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

@@ -4,6 +4,9 @@ import com.ffii.core.support.AbstractRepository
import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo
import com.ffii.fpsms.modules.stock.web.model.StockOutStatus import com.ffii.fpsms.modules.stock.web.model.StockOutStatus
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import org.springframework.data.repository.query.Param
import java.time.LocalDate
import org.springframework.data.jpa.repository.Query


@Repository @Repository
interface StockOutLIneRepository: AbstractRepository<StockOutLine, Long> { interface StockOutLIneRepository: AbstractRepository<StockOutLine, Long> {
@@ -21,5 +24,27 @@ interface StockOutLIneRepository: AbstractRepository<StockOutLine, Long> {
inventoryLotLineId: Long inventoryLotLineId: Long
): List<StockOutLine> ): List<StockOutLine>
fun existsByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(pickOrderLineId: Long, inventoryLotLineId: Long): Boolean fun existsByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(pickOrderLineId: Long, inventoryLotLineId: Long): Boolean
//fun findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId: Long): List<StockOutLine>
@Query("""
SELECT sol FROM StockOutLine sol
WHERE sol.deleted = false
AND sol.type IS NOT NULL
AND (:startDate IS NULL OR DATE(sol.created) >= :startDate)
AND (:endDate IS NULL OR DATE(sol.created) <= :endDate)
AND (:itemCode IS NULL OR sol.item.code LIKE %:itemCode%)
AND (:itemName IS NULL OR sol.item.name LIKE %:itemName%)
AND (:type IS NULL OR sol.type = :type)
ORDER BY sol.created DESC
""")
fun searchStockOutLines(
@Param("startDate") startDate: LocalDate?,
@Param("endDate") endDate: LocalDate?,
@Param("itemCode") itemCode: String?,
@Param("itemName") itemName: String?,
@Param("type") type: String?
): List<StockOutLine>
@Query("SELECT sol FROM StockOutLine sol WHERE sol.item.id = :itemId AND sol.deleted = false")
fun findAllByItemIdAndDeletedFalse(itemId: Long): List<StockOutLine>

@Query("SELECT sol FROM StockOutLine sol WHERE sol.item.id IN :itemIds AND sol.deleted = false")
fun findAllByItemIdInAndDeletedFalse(itemIds: List<Long>): List<StockOutLine>
} }

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

@@ -48,4 +48,8 @@ open class StockOutLine: BaseEntity<Long>() {


@Column(name = "type") @Column(name = "type")
open var type: String? = null open var type: String? = null
@Column(name = "startTime")
open var startTime: LocalDateTime? = null
@Column(name = "endTime")
open var endTime: LocalDateTime? = null
} }

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

@@ -7,4 +7,5 @@ import java.util.Optional
@Repository @Repository
interface StockOutRepository: AbstractRepository<StockOut, Long> { interface StockOutRepository: AbstractRepository<StockOut, Long> {
fun findByConsoPickOrderCode(consoPickOrderCode: String) : Optional<StockOut> fun findByConsoPickOrderCode(consoPickOrderCode: String) : Optional<StockOut>
fun findByStockTakeIdAndDeletedFalse(stockTakeId: Long): StockOut?
} }

+ 22
- 11
src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt Wyświetl plik

@@ -5,20 +5,31 @@ import com.ffii.core.support.AbstractRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.io.Serializable import java.io.Serializable
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import java.time.LocalDate
@Repository @Repository
interface StockTakeRecordRepository : AbstractRepository<StockTakeRecord, Long> { interface StockTakeRecordRepository : AbstractRepository<StockTakeRecord, Long> {
fun findAllByStockTakeIdAndDeletedIsFalse(stockTakeId: Long): List<StockTakeRecord>; fun findAllByStockTakeIdAndDeletedIsFalse(stockTakeId: Long): List<StockTakeRecord>;
fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeRecord?; fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeRecord?;

@Query(""" @Query("""
SELECT w.stockTakeSection, str.stockTake.id, COUNT(str.id) as count
FROM StockTakeRecord str
INNER JOIN Warehouse w ON str.warehouse.id = w.id
WHERE str.deleted = false
AND w.deleted = false
AND w.stockTakeSection IS NOT NULL
AND w.stockTakeSection != ''
AND str.stockTake.id IS NOT NULL
GROUP BY w.stockTakeSection, str.stockTake.id
SELECT sl FROM StockLedger sl
LEFT JOIN sl.stockInLine sil
LEFT JOIN sl.stockOutLine sol
LEFT JOIN sl.inventory inv
LEFT JOIN inv.item i
WHERE sl.deleted = false
AND (:itemCode IS NULL OR sl.itemCode LIKE CONCAT('%', :itemCode, '%'))
AND (:itemName IS NULL OR i.name LIKE CONCAT('%', :itemName, '%'))
AND (:type IS NULL OR sl.type = :type)
AND (:startDate IS NULL OR DATE(sl.created) >= :startDate)
AND (:endDate IS NULL OR DATE(sl.created) <= :endDate)
ORDER BY COALESCE(sl.date, DATE(sl.created)) ASC, sl.created ASC, sl.itemId
""") """)
fun countStockTakeRecordsBySectionAndStockTakeId(): List<Array<Any>>
}
fun countStockTakeRecordsBySectionAndStockTakeId(): List<Array<Any>>




}


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

@@ -48,7 +48,9 @@ import java.io.File
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintQrCodeForDoRequest import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintQrCodeForDoRequest

import com.ffii.fpsms.modules.stock.service.InventoryLotLineService
import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
@Serializable @Serializable
data class QrContent(val itemId: Long, val stockInLineId: Long) data class QrContent(val itemId: Long, val stockInLineId: Long)


@@ -72,7 +74,10 @@ open class StockInLineService(
private val itemUomRepository: ItemUomRespository, private val itemUomRepository: ItemUomRespository,
private val printerService: PrinterService, private val printerService: PrinterService,
private val stockTakeLineRepository: StockTakeLineRepository, private val stockTakeLineRepository: StockTakeLineRepository,
private val inventoryLotLineService: InventoryLotLineService,
private val deliveryOrderRepository: DeliveryOrderRepository, private val deliveryOrderRepository: DeliveryOrderRepository,
private val stockLedgerRepository: StockLedgerRepository,
private val inventoryRepository: InventoryRepository
): AbstractBaseEntityService<StockInLine, Long, StockInLineRepository>(jdbcDao, stockInLineRepository) { ): AbstractBaseEntityService<StockInLine, Long, StockInLineRepository>(jdbcDao, stockInLineRepository) {


open fun getStockInLineInfo(stockInLineId: Long): StockInLineInfo { open fun getStockInLineInfo(stockInLineId: Long): StockInLineInfo {
@@ -113,7 +118,7 @@ open class StockInLineService(
// stockIn = stockInService.create(SaveStockInRequest(purchaseOrderId = request.purchaseOrderId)).entity as StockIn // stockIn = stockInService.create(SaveStockInRequest(purchaseOrderId = request.purchaseOrderId)).entity as StockIn
// var stockIn = stockInRepository.findByPurchaseOrderIdAndDeletedFalse(request.purchaseOrderId) // var stockIn = stockInRepository.findByPurchaseOrderIdAndDeletedFalse(request.purchaseOrderId)
} }
createStockLedgerForStockIn(stockInLine)
val item = itemRepository.findById(request.itemId).orElseThrow() val item = itemRepository.findById(request.itemId).orElseThrow()
// If request contains valid POL // If request contains valid POL
if (pol != null) { if (pol != null) {
@@ -153,6 +158,7 @@ open class StockInLineService(
// if (pol.qty!! < request.acceptedQty) { // if (pol.qty!! < request.acceptedQty) {
// throw BadRequestException() // throw BadRequestException()
// } // }

stockInLine.apply { stockInLine.apply {
this.item = item this.item = item
itemNo = item.code itemNo = item.code
@@ -162,6 +168,7 @@ open class StockInLineService(
if (jo != null && jo?.bom != null) { if (jo != null && jo?.bom != null) {
// For job orders, demandQty comes from BOM's outputQty // For job orders, demandQty comes from BOM's outputQty
this.demandQty = jo?.bom?.outputQty this.demandQty = jo?.bom?.outputQty
} else if (pol != null) { } else if (pol != null) {
// For purchase orders, demandQty comes from PurchaseOrderLine's qty // For purchase orders, demandQty comes from PurchaseOrderLine's qty
this.demandQty = pol.qty this.demandQty = pol.qty
@@ -169,6 +176,7 @@ open class StockInLineService(
dnNo = request.dnNo dnNo = request.dnNo
receiptDate = request.receiptDate?.atStartOfDay() ?: LocalDateTime.now() receiptDate = request.receiptDate?.atStartOfDay() ?: LocalDateTime.now()
productLotNo = request.productLotNo productLotNo = request.productLotNo
this.type = "Nor"
status = StockInLineStatus.PENDING.status status = StockInLineStatus.PENDING.status
} }
val savedSIL = saveAndFlush(stockInLine) val savedSIL = saveAndFlush(stockInLine)
@@ -398,6 +406,7 @@ open class StockInLineService(
} }
// TODO: check all status to prevent reverting progress due to multiple users access to the same po? // TODO: check all status to prevent reverting progress due to multiple users access to the same po?
// return list of stock in line, update data grid with the list // return list of stock in line, update data grid with the list
createStockLedgerForStockIn(stockInLine)
stockInLine.apply { stockInLine.apply {
this.productionDate = request.productionDate?.atStartOfDay() ?: this.productionDate this.productionDate = request.productionDate?.atStartOfDay() ?: this.productionDate
this.productLotNo = request.productLotNo ?: this.productLotNo this.productLotNo = request.productLotNo ?: this.productLotNo
@@ -646,4 +655,80 @@ open class StockInLineService(
} }
} }
} }
open fun newStockInCreate(request: NewStockInCreateRequest): MessageResponse {
val newLotNo = if (request.lotNo == null) assignLotNo() else request.lotNo
val inventoryLotLine = inventoryLotLineRepository.findByIdAndDeletedIsFalse(
request.inventoryLotLineId ?: throw IllegalArgumentException("Inventory lot ID not found")
) ?: throw IllegalArgumentException("Inventory lot line not found")
val inventoryLot = inventoryLotRepository.findByIdAndDeletedFalse(
inventoryLotLine?.inventoryLot?.id ?: throw IllegalArgumentException("Inventory lot ID not found")
) ?: throw IllegalArgumentException("Inventory lot not found")
val stockIn=StockIn().apply {
this.code=stockTake?.code
this.status=request.stockIntype
//this.stockTake=stockTake
}
stockInRepository.save(stockIn)
val stockInLine = StockInLine().apply {
//this.stockTakeLine=stockTakeLine
this.item=inventoryLot.item
this.itemNo=request.itemNo
this.stockIn = stockIn
this.demandQty=request.demandQty
this.acceptedQty=request.acceptedQty
this.expiryDate=inventoryLot.expiryDate
this.inventoryLot=inventoryLot
this.inventoryLotLine=inventoryLotLine
this.lotNo=newLotNo
this.status = "completed"
this.type = request.stockInLineType
}
stockInLineRepository.save(stockInLine)
val updateRequest = SaveInventoryLotLineRequest(
id = inventoryLotLine.id,
inventoryLotId = inventoryLotLine.inventoryLot?.id,
warehouseId = inventoryLotLine.warehouse?.id,
stockUomId = inventoryLotLine.stockUom?.id,
inQty = request.acceptedQty,
outQty = inventoryLotLine.outQty,
holdQty = inventoryLotLine.holdQty,
status = inventoryLotLine.status?.value,
remarks = inventoryLotLine.remarks
)
inventoryLotLineService.saveInventoryLotLine(updateRequest)
return MessageResponse(
id = stockInLine.id,
code = null,
name = stockInLine.item?.name,
type = "success",
message = "Stock in line created successfully",
errorPosition = null
)
}
@Transactional
private fun createStockLedgerForStockIn(stockInLine: StockInLine) {
val item = stockInLine.item ?: return
val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return
val inQty = stockInLine.acceptedQty?.toDouble() ?: 0.0
// 直接使用 inventory.onHandQty 作为 balance(已经是更新后的值)
val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
val stockLedger = StockLedger().apply {
this.stockInLine = stockInLine
this.inventory = inventory
this.inQty = inQty
this.outQty = null
this.balance = newBalance
this.type = stockInLine.type
this.itemId = item.id
this.itemCode = item.code
this.date = LocalDate.now()
}
stockLedgerRepository.saveAndFlush(stockLedger)
}
} }

+ 56
- 13
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt Wyświetl plik

@@ -37,6 +37,8 @@ import com.ffii.fpsms.modules.bag.web.model.CreateBagLotLineRequest
import com.ffii.fpsms.modules.common.CodeGenerator import com.ffii.fpsms.modules.common.CodeGenerator
import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Lazy
import com.ffii.fpsms.modules.bag.service.BagService import com.ffii.fpsms.modules.bag.service.BagService
import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository


import java.time.LocalTime import java.time.LocalTime
@Service @Service
@@ -46,7 +48,6 @@ open class StockOutLineService(
private val stockOutRepository: StockOutRepository, private val stockOutRepository: StockOutRepository,
private val stockOutLineRepository: StockOutLIneRepository, private val stockOutLineRepository: StockOutLIneRepository,
private val itemRepository: ItemsRepository, private val itemRepository: ItemsRepository,
private val inventoryRepository: InventoryRepository,
private val itemUomRespository: ItemUomRespository, private val itemUomRespository: ItemUomRespository,
private val pickOrderRepository: PickOrderRepository, private val pickOrderRepository: PickOrderRepository,
private val inventoryLotLineRepository: InventoryLotLineRepository, private val inventoryLotLineRepository: InventoryLotLineRepository,
@@ -59,7 +60,9 @@ private val deliveryOrderRepository: DeliveryOrderRepository,
private val doPickOrderLineRepository: DoPickOrderLineRepository, private val doPickOrderLineRepository: DoPickOrderLineRepository,
private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository, private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository,
private val inventoryLotLineService: InventoryLotLineService, private val inventoryLotLineService: InventoryLotLineService,
private val bagService: BagService
private val bagService: BagService,
private val stockLedgerRepository: StockLedgerRepository,
private val inventoryRepository: InventoryRepository
): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) { ): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) {
@Throws(IOException::class) @Throws(IOException::class)
@Transactional @Transactional
@@ -135,6 +138,7 @@ val existingStockOutLine = stockOutLineRepository.findByPickOrderLineIdAndInvent
) )
val item = itemRepository.findById(pickOrderLine.item!!.id!!).orElseThrow() val item = itemRepository.findById(pickOrderLine.item!!.id!!).orElseThrow()
val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow()
val stockOutLine = StockOutLine() val stockOutLine = StockOutLine()
.apply { .apply {
this.item = item this.item = item
@@ -143,8 +147,10 @@ val existingStockOutLine = stockOutLineRepository.findByPickOrderLineIdAndInvent
this.inventoryLotLine = inventoryLotLine this.inventoryLotLine = inventoryLotLine
this.pickOrderLine = pickOrderLine this.pickOrderLine = pickOrderLine
this.status = StockOutLineStatus.PENDING.status this.status = StockOutLineStatus.PENDING.status
this.type = "Nor"
} }
val savedStockOutLine = saveAndFlush(stockOutLine) val savedStockOutLine = saveAndFlush(stockOutLine)
createStockLedgerForStockOut(savedStockOutLine)
val mappedSavedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!) val mappedSavedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!)
// println("triggering") // println("triggering")
return MessageResponse( return MessageResponse(
@@ -231,7 +237,7 @@ open fun createWithoutConso(request: CreateStockOutLineWithoutConsoRequest): Mes
IllegalArgumentException("InventoryLotLine not found with ID: ${request.inventoryLotLineId}") IllegalArgumentException("InventoryLotLine not found with ID: ${request.inventoryLotLineId}")
} }
println("Found inventoryLotLine: ${inventoryLotLine.id}") println("Found inventoryLotLine: ${inventoryLotLine.id}")
val stockOutLine = StockOutLine() val stockOutLine = StockOutLine()
.apply { .apply {
this.item = item this.item = item
@@ -240,10 +246,12 @@ open fun createWithoutConso(request: CreateStockOutLineWithoutConsoRequest): Mes
this.inventoryLotLine = inventoryLotLine this.inventoryLotLine = inventoryLotLine
this.pickOrderLine = updatedPickOrderLine this.pickOrderLine = updatedPickOrderLine
this.status = StockOutLineStatus.PENDING.status this.status = StockOutLineStatus.PENDING.status
this.type = "Nor"
} }
println("Created stockOutLine with qty: ${request.qty}") println("Created stockOutLine with qty: ${request.qty}")
val savedStockOutLine = saveAndFlush(stockOutLine) val savedStockOutLine = saveAndFlush(stockOutLine)
createStockLedgerForStockOut(savedStockOutLine)
println("Saved stockOutLine with ID: ${savedStockOutLine.id}") println("Saved stockOutLine with ID: ${savedStockOutLine.id}")
val mappedSavedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!) val mappedSavedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!)
@@ -536,7 +544,12 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
println("Updating StockOutLine ID: ${request.id}") println("Updating StockOutLine ID: ${request.id}")
println("Current status: ${stockOutLine.status}") println("Current status: ${stockOutLine.status}")
println("New status: ${request.status}") println("New status: ${request.status}")
if (request.status == "checked") {
stockOutLine.startTime = LocalDateTime.now()
}
if (request.status == "completed") {
stockOutLine.endTime = LocalDateTime.now()
}
// 2. 更新自身 status/qty // 2. 更新自身 status/qty
stockOutLine.status = request.status stockOutLine.status = request.status
if (request.qty != null) { if (request.qty != null) {
@@ -582,14 +595,14 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
bagService.createBagLotLinesByBagId(createBagLotLineRequest) bagService.createBagLotLinesByBagId(createBagLotLineRequest)
println(" ✓ BagLotLine created successfully for item ${item.code}") println(" ✓ BagLotLine created successfully for item ${item.code}")
} else { } else {
println(" ⚠️ Warning: lotNo is null, skipping BagLotLine creation")
println(" Warning: lotNo is null, skipping BagLotLine creation")
} }
} else { } else {
println(" ⚠️ Warning: Bag not found for itemId ${item.id}, skipping BagLotLine creation")
println(" Warning: Bag not found for itemId ${item.id}, skipping BagLotLine creation")
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
println(" ⚠️ Error creating BagLotLine: ${e.message}")
println(" Error creating BagLotLine: ${e.message}")
e.printStackTrace() e.printStackTrace()
// 不中断主流程,只记录错误 // 不中断主流程,只记录错误
} }
@@ -891,7 +904,7 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta
) )
} }
println("🔍 Checking inventory lot:")
println(" Checking inventory lot:")
println(" - Found inventory lot:") println(" - Found inventory lot:")
println(" - Lot No: ${inventoryLot.lotNo}") println(" - Lot No: ${inventoryLot.lotNo}")
println(" - Item ID: ${inventoryLot.item?.id}") println(" - Item ID: ${inventoryLot.item?.id}")
@@ -906,10 +919,16 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta
println(" - Item ID match: $itemIdMatch (Expected: ${inventoryLot.item?.id}, Got: ${request.itemId})") println(" - Item ID match: $itemIdMatch (Expected: ${inventoryLot.item?.id}, Got: ${request.itemId})")


if (lotNoMatch && itemIdMatch) { if (lotNoMatch && itemIdMatch) {
// 匹配成功,更新状态
println(" MATCH SUCCESS: Lot and Item both match!") println(" MATCH SUCCESS: Lot and Item both match!")
stockOutLine.status = request.status stockOutLine.status = request.status
if (request.status == "checked") {
stockOutLine.startTime = LocalDateTime.now()
}
if (request.status == "completed") {
stockOutLine.endTime = LocalDateTime.now()
}
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
println(" Status updated successfully:") println(" Status updated successfully:")
@@ -1099,14 +1118,14 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
bagService.createBagLotLinesByBagId(createBagLotLineRequest) bagService.createBagLotLinesByBagId(createBagLotLineRequest)
println(" ✓ BagLotLine created successfully for item ${item.code}") println(" ✓ BagLotLine created successfully for item ${item.code}")
} else { } else {
println(" ⚠️ Warning: lotNo is null, skipping BagLotLine creation")
println(" Warning: lotNo is null, skipping BagLotLine creation")
} }
} else { } else {
println(" ⚠️ Warning: Bag not found for itemId ${item.id}, skipping BagLotLine creation")
println(" Warning: Bag not found for itemId ${item.id}, skipping BagLotLine creation")
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
println(" ⚠️ Error creating BagLotLine: ${e.message}")
println(" Error creating BagLotLine: ${e.message}")
e.printStackTrace() e.printStackTrace()
// 不中断主流程,只记录错误 // 不中断主流程,只记录错误
} }
@@ -1165,4 +1184,28 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
) )
) )
} }
}}
}
@Transactional
private fun createStockLedgerForStockOut(stockOutLine: StockOutLine) {
val item = stockOutLine.item ?: return
val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return
val outQty = stockOutLine.qty?.toDouble() ?: 0.0
// 直接使用 inventory.onHandQty 作为 balance(已经是更新后的值)
val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
val stockLedger = StockLedger().apply {
this.stockOutLine = stockOutLine
this.inventory = inventory
this.inQty = null
this.outQty = outQty
this.balance = newBalance
this.type = stockOutLine.type
this.itemId = item.id
this.itemCode = item.code
this.date = LocalDate.now()
}
stockLedgerRepository.saveAndFlush(stockLedger)
}
}

+ 297
- 120
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt Wyświetl plik

@@ -14,7 +14,9 @@ import org.springframework.stereotype.Service
import java.time.LocalDateTime import java.time.LocalDateTime
import java.math.BigDecimal import java.math.BigDecimal
import com.ffii.fpsms.modules.user.entity.UserRepository import com.ffii.fpsms.modules.user.entity.UserRepository

import org.springframework.data.domain.PageRequest
import com.ffii.core.response.RecordsRes
import java.time.LocalDate
import com.ffii.fpsms.modules.stock.service.InventoryLotLineService import com.ffii.fpsms.modules.stock.service.InventoryLotLineService
import com.ffii.fpsms.modules.stock.entity.StockTakeLine import com.ffii.fpsms.modules.stock.entity.StockTakeLine
import com.ffii.fpsms.modules.stock.entity.StockTakeLineRepository import com.ffii.fpsms.modules.stock.entity.StockTakeLineRepository
@@ -29,6 +31,16 @@ import com.ffii.fpsms.modules.stock.entity.StockInLine
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.modules.stock.entity.InventoryLot import com.ffii.fpsms.modules.stock.entity.InventoryLot
import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository
import jakarta.persistence.EntityManager
import jakarta.persistence.PersistenceContext
import org.hibernate.Session
import org.hibernate.jdbc.Work
import java.sql.Connection
import java.sql.PreparedStatement
import java.sql.ResultSet
import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import com.ffii.fpsms.modules.stock.entity.StockLedger
@Service @Service
class StockTakeRecordService( class StockTakeRecordService(
val stockTakeRepository: StockTakeRepository, val stockTakeRepository: StockTakeRepository,
@@ -42,7 +54,10 @@ class StockTakeRecordService(
val stockOutLineRepository: StockOutLIneRepository, val stockOutLineRepository: StockOutLIneRepository,
val stockInRepository: StockInRepository, val stockInRepository: StockInRepository,
val stockInLineRepository: StockInLineRepository, val stockInLineRepository: StockInLineRepository,
val inventoryLotRepository: InventoryLotRepository
val inventoryLotRepository: InventoryLotRepository,
val stockLedgerRepository: StockLedgerRepository,
val inventoryRepository: InventoryRepository

) { ) {
private val logger: Logger = LoggerFactory.getLogger(StockTakeRecordService::class.java) private val logger: Logger = LoggerFactory.getLogger(StockTakeRecordService::class.java)


@@ -138,6 +153,7 @@ class StockTakeRecordService(
} }
// 9. 计算 TotalItemNumber:获取该 section 下所有 InventoryLotLine,按 item 分组,计算不同的 item 数量 // 9. 计算 TotalItemNumber:获取该 section 下所有 InventoryLotLine,按 item 分组,计算不同的 item 数量
val totalItemNumber = inventoryLotLineRepository.countDistinctItemsByWarehouseIds(warehouseIds).toInt() val totalItemNumber = inventoryLotLineRepository.countDistinctItemsByWarehouseIds(warehouseIds).toInt()
val totalInventoryLotNumber = inventoryLotLineRepository.countAllByWarehouseIds(warehouseIds).toInt()
// 8. 使用 stockTakeSection 作为 stockTakeSession // 8. 使用 stockTakeSection 作为 stockTakeSession
val reStockTakeTrueFalse = if (latestStockTake != null) { val reStockTakeTrueFalse = if (latestStockTake != null) {
// 检查该 stock take 下该 section 的记录中是否有 notMatch 状态 // 检查该 stock take 下该 section 的记录中是否有 notMatch 状态
@@ -158,7 +174,7 @@ class StockTakeRecordService(
lastStockTakeDate = latestStockTake?.actualStart?.toLocalDate(), lastStockTakeDate = latestStockTake?.actualStart?.toLocalDate(),
status = status?:"", status = status?:"",
currentStockTakeItemNumber = 0, currentStockTakeItemNumber = 0,
totalInventoryLotNumber = 0,
totalInventoryLotNumber = totalInventoryLotNumber,
stockTakeId = latestStockTake?.id ?: 0, stockTakeId = latestStockTake?.id ?: 0,
stockTakerName = stockTakerName, stockTakerName = stockTakerName,
TotalItemNumber = totalItemNumber, TotalItemNumber = totalItemNumber,
@@ -286,6 +302,7 @@ class StockTakeRecordService(
} }
// 9. 计算 TotalItemNumber:获取该 section 下所有 InventoryLotLine,按 item 分组,计算不同的 item 数量 // 9. 计算 TotalItemNumber:获取该 section 下所有 InventoryLotLine,按 item 分组,计算不同的 item 数量
val totalItemNumber = inventoryLotLineRepository.countDistinctItemsByWarehouseIds(warehouseIds).toInt() val totalItemNumber = inventoryLotLineRepository.countDistinctItemsByWarehouseIds(warehouseIds).toInt()
val totalInventoryLotNumber = inventoryLotLineRepository.countAllByWarehouseIds(warehouseIds).toInt()
// 9. 使用 stockTakeSection 作为 stockTakeSession // 9. 使用 stockTakeSection 作为 stockTakeSession
result.add( result.add(
AllPickedStockTakeListReponse( AllPickedStockTakeListReponse(
@@ -294,7 +311,7 @@ class StockTakeRecordService(
lastStockTakeDate = latestStockTake?.actualStart?.toLocalDate(), lastStockTakeDate = latestStockTake?.actualStart?.toLocalDate(),
status = status?:"", status = status?:"",
currentStockTakeItemNumber = 0, // 临时设为 0,测试性能 currentStockTakeItemNumber = 0, // 临时设为 0,测试性能
totalInventoryLotNumber = 0, // 临时设为 0,测试性能
totalInventoryLotNumber = totalInventoryLotNumber, // 临时设为 0,测试性能
stockTakeId = latestStockTake?.id ?: 0, stockTakeId = latestStockTake?.id ?: 0,
stockTakerName = stockTakerName, stockTakerName = stockTakerName,
approverName = approverName, approverName = approverName,
@@ -376,92 +393,103 @@ class StockTakeRecordService(
) )
} }
} }
open fun getInventoryLotDetailsByStockTakeSection(stockTakeSection: String, stockTakeId: Long? = null): List<InventoryLotDetailResponse> {
println("getInventoryLotDetailsByStockTakeSection called with section: $stockTakeSection, stockTakeId: $stockTakeId")
val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection)
if (warehouses.isEmpty()) {
logger.warn("No warehouses found for stockTakeSection: $stockTakeSection")
return emptyList()
}
val warehouseIds = warehouses.mapNotNull { it.id }
println("Found ${warehouses.size} warehouses for section $stockTakeSection")
val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds)
println("Found ${inventoryLotLines.size} inventory lot lines")
val stockTakeRecordsMap = if (stockTakeId != null) {
val allStockTakeRecords = stockTakeRecordRepository.findAll()
.filter {
!it.deleted &&
it.stockTake?.id == stockTakeId &&
it.warehouse?.id in warehouseIds
}
// 按 lotId 和 warehouseId 建立映射
allStockTakeRecords.associateBy {
Pair(it.lotId ?: 0L, it.warehouse?.id ?: 0L)
open fun getInventoryLotDetailsByStockTakeSection(
stockTakeSection: String,
stockTakeId: Long? = null,
pageNum: Int = 0,
pageSize: Int = 10
): RecordsRes<InventoryLotDetailResponse> {
println("getInventoryLotDetailsByStockTakeSection called with section: $stockTakeSection, stockTakeId: $stockTakeId, pageNum: $pageNum, pageSize: $pageSize")
val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection)
if (warehouses.isEmpty()) {
logger.warn("No warehouses found for stockTakeSection: $stockTakeSection")
return RecordsRes(emptyList(), 0)
}
val warehouseIds = warehouses.mapNotNull { it.id }
println("Found ${warehouses.size} warehouses for section $stockTakeSection")
val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds)
println("Found ${inventoryLotLines.size} inventory lot lines")
val stockTakeRecordsMap = if (stockTakeId != null) {
val allStockTakeRecords = stockTakeRecordRepository.findAll()
.filter {
!it.deleted &&
it.stockTake?.id == stockTakeId &&
it.warehouse?.id in warehouseIds
} }
} else {
emptyMap()
// 按 lotId 和 warehouseId 建立映射
allStockTakeRecords.associateBy {
Pair(it.lotId ?: 0L, it.warehouse?.id ?: 0L)
} }
} else {
emptyMap()
}
val allResults = inventoryLotLines.map { ill ->
val inventoryLot = ill.inventoryLot
val item = inventoryLot?.item
val warehouse = ill.warehouse
val availableQty = (ill.inQty ?: BigDecimal.ZERO)
.subtract(ill.outQty ?: BigDecimal.ZERO)
.subtract(ill.holdQty ?: BigDecimal.ZERO)
return inventoryLotLines.map { ill ->
val inventoryLot = ill.inventoryLot
val item = inventoryLot?.item
val warehouse = ill.warehouse
val availableQty = (ill.inQty ?: BigDecimal.ZERO)
.subtract(ill.outQty ?: BigDecimal.ZERO)
.subtract(ill.holdQty ?: BigDecimal.ZERO)
val stockTakeRecord = if (stockTakeId != null && inventoryLot?.id != null && warehouse?.id != null) {
stockTakeRecordsMap[Pair(inventoryLot.id, warehouse.id)]
} else {
null
}
val inventoryLotLineId = ill.id
val stockTakeLine = stockTakeLineRepository.findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(inventoryLotLineId, stockTakeId!!)
InventoryLotDetailResponse(
id = ill.id ?: 0L,
inventoryLotId = inventoryLot?.id ?: 0L,
itemId = item?.id ?: 0L,
itemCode = item?.code,
itemName = item?.name,
lotNo = inventoryLot?.lotNo,
expiryDate = inventoryLot?.expiryDate,
productionDate = inventoryLot?.productionDate,
stockInDate = inventoryLot?.stockInDate,
inQty = ill.inQty,
remarks = stockTakeRecord?.remarks,
outQty = ill.outQty,
holdQty = ill.holdQty,
availableQty = availableQty,
uom = ill.stockUom?.uom?.udfudesc,
warehouseCode = warehouse?.code,
warehouseName = warehouse?.name,
status = ill.status?.name,
warehouseSlot = warehouse?.slot,
warehouseArea = warehouse?.area,
warehouse = warehouse?.warehouse,
varianceQty = stockTakeRecord?.varianceQty,
stockTakeRecordId = stockTakeRecord?.id,
stockTakeRecordStatus = stockTakeRecord?.status,
firstStockTakeQty = stockTakeRecord?.pickerFirstStockTakeQty,
secondStockTakeQty = stockTakeRecord?.pickerSecondStockTakeQty,
firstBadQty = stockTakeRecord?.pickerFirstBadQty,
secondBadQty = stockTakeRecord?.pickerSecondBadQty,
approverQty = stockTakeRecord?.approverStockTakeQty ,
approverBadQty = stockTakeRecord?.approverBadQty,
finalQty = stockTakeLine?.finalQty,
//finalQty = null,
)
val stockTakeRecord = if (stockTakeId != null && inventoryLot?.id != null && warehouse?.id != null) {
stockTakeRecordsMap[Pair(inventoryLot.id, warehouse.id)]
} else {
null
} }
val inventoryLotLineId = ill.id
val stockTakeLine = stockTakeLineRepository.findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(inventoryLotLineId, stockTakeId!!)
InventoryLotDetailResponse(
id = ill.id ?: 0L,
inventoryLotId = inventoryLot?.id ?: 0L,
itemId = item?.id ?: 0L,
itemCode = item?.code,
itemName = item?.name,
lotNo = inventoryLot?.lotNo,
expiryDate = inventoryLot?.expiryDate,
productionDate = inventoryLot?.productionDate,
stockInDate = inventoryLot?.stockInDate,
inQty = ill.inQty,
remarks = stockTakeRecord?.remarks,
outQty = ill.outQty,
holdQty = ill.holdQty,
availableQty = availableQty,
uom = ill.stockUom?.uom?.udfudesc,
warehouseCode = warehouse?.code,
warehouseName = warehouse?.name,
status = ill.status?.name,
warehouseSlot = warehouse?.slot,
warehouseArea = warehouse?.area,
warehouse = warehouse?.warehouse,
varianceQty = stockTakeRecord?.varianceQty,
stockTakeRecordId = stockTakeRecord?.id,
stockTakeRecordStatus = stockTakeRecord?.status,
firstStockTakeQty = stockTakeRecord?.pickerFirstStockTakeQty,
secondStockTakeQty = stockTakeRecord?.pickerSecondStockTakeQty,
firstBadQty = stockTakeRecord?.pickerFirstBadQty,
secondBadQty = stockTakeRecord?.pickerSecondBadQty,
approverQty = stockTakeRecord?.approverStockTakeQty ,
approverBadQty = stockTakeRecord?.approverBadQty,
finalQty = stockTakeLine?.finalQty,
)
} }
// Apply pagination
val pageable = PageRequest.of(pageNum, pageSize)
val startIndex = pageable.offset.toInt()
val endIndex = minOf(startIndex + pageSize, allResults.size)
val paginatedResult = if (startIndex < allResults.size) {
allResults.subList(startIndex, endIndex)
} else {
emptyList()
}
return RecordsRes(paginatedResult, allResults.size)
}
open fun saveStockTakeRecord( open fun saveStockTakeRecord(
request: SaveStockTakeRecordRequest, request: SaveStockTakeRecordRequest,
stockTakeId: Long, stockTakeId: Long,
@@ -750,30 +778,32 @@ open fun checkAndUpdateStockTakeStatus(stockTakeId: Long, stockTakeSection: Stri
val allRecordsCompleted = stockTakeRecords.isNotEmpty() && val allRecordsCompleted = stockTakeRecords.isNotEmpty() &&
stockTakeRecords.all { it.status == "completed" } stockTakeRecords.all { it.status == "completed" }
// 6. 如果所有记录都已创建且都是 "pass",更新 stock take 状态为 "approving" // 6. 如果所有记录都已创建且都是 "pass",更新 stock take 状态为 "approving"
if (allLinesHaveRecords && allRecordsPassed) {
if (allLinesHaveRecords && allRecordsCompleted) {
stockTake.status = StockTakeStatus.COMPLETED
stockTake.planEnd = java.time.LocalDateTime.now()
stockTakeRepository.save(stockTake)
println("Stock take $stockTakeId status updated to COMPLETED - all records are completed")
return mapOf("success" to true, "message" to "Stock take status updated to COMPLETED", "updated" to true)
} else if (allLinesHaveRecords && allRecordsPassed) {
// 如果所有记录都已创建且都是 "pass" 或 "completed",更新 stock take 状态为 "approving"
stockTake.status = StockTakeStatus.APPROVING stockTake.status = StockTakeStatus.APPROVING
stockTake.actualEnd = java.time.LocalDateTime.now() stockTake.actualEnd = java.time.LocalDateTime.now()
stockTakeRepository.save(stockTake) stockTakeRepository.save(stockTake)

println("Stock take $stockTakeId status updated to APPROVING - all records are pass") println("Stock take $stockTakeId status updated to APPROVING - all records are pass")
return mapOf( return mapOf(
"success" to true, "success" to true,
"message" to "Stock take status updated to APPROVING", "message" to "Stock take status updated to APPROVING",
"updated" to true "updated" to true
) )
} else if (allLinesHaveRecords && allRecordsCompleted) {
stockTake.status = StockTakeStatus.COMPLETED
stockTake.planEnd = java.time.LocalDateTime.now()
stockTakeRepository.save(stockTake)
println("Stock take $stockTakeId status updated to COMPLETED - all records are completed")
return mapOf("success" to true, "message" to "Stock take status updated to COMPLETED", "updated" to true)
} else { } else {
return mapOf( return mapOf(
"success" to true, "success" to true,
"message" to "Conditions not met for status update", "message" to "Conditions not met for status update",
"updated" to false, "updated" to false,
"allLinesHaveRecords" to allLinesHaveRecords, "allLinesHaveRecords" to allLinesHaveRecords,
"allRecordsPassed" to allRecordsPassed
"allRecordsPassed" to allRecordsPassed,
"allRecordsCompleted" to allRecordsCompleted
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -835,6 +865,8 @@ open fun saveApproverStockTakeRecord(
val inventoryLot = inventoryLotRepository.findByIdAndDeletedFalse( val inventoryLot = inventoryLotRepository.findByIdAndDeletedFalse(
inventoryLotLine?.inventoryLot?.id ?: throw IllegalArgumentException("Inventory lot ID not found") inventoryLotLine?.inventoryLot?.id ?: throw IllegalArgumentException("Inventory lot ID not found")
) ?: throw IllegalArgumentException("Inventory lot not found") ) ?: throw IllegalArgumentException("Inventory lot not found")
val inventory = inventoryRepository.findByItemId(inventoryLot.item?.id ?: throw IllegalArgumentException("Item ID not found"))
.orElse(null) ?: throw IllegalArgumentException("Inventory not found for item")


if (varianceQty !=BigDecimal.ZERO ) { if (varianceQty !=BigDecimal.ZERO ) {
val stockTakeLine = StockTakeLine().apply { val stockTakeLine = StockTakeLine().apply {
@@ -849,13 +881,14 @@ open fun saveApproverStockTakeRecord(
stockTakeLineRepository.save(stockTakeLine) stockTakeLineRepository.save(stockTakeLine)


if (varianceQty < BigDecimal.ZERO ) { if (varianceQty < BigDecimal.ZERO ) {
val stockOut=StockOut().apply {
this.type="stockTake"
this.status="completed"
this.handler=request.approverId
}
stockOutRepository.save(stockOut)

var stockOut = stockOutRepository.findByStockTakeIdAndDeletedFalse(stockTakeId)
?: StockOut().apply {
this.type = "stockTake"
this.status = "completed"
this.handler = request.approverId
}.also { stockOutRepository.save(it) }
val stockOutLine = StockOutLine().apply { val stockOutLine = StockOutLine().apply {
this.item=inventoryLot.item this.item=inventoryLot.item
this.qty=(-varianceQty)?.toDouble() this.qty=(-varianceQty)?.toDouble()
@@ -866,21 +899,35 @@ open fun saveApproverStockTakeRecord(
} }
stockOutLineRepository.save(stockOutLine) stockOutLineRepository.save(stockOutLine)
val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()-(varianceQty).toDouble()
val stockLedger = StockLedger().apply {
this.inventory=inventory
this.itemId=inventoryLot.item?.id
this.itemCode=stockTakeRecord.itemCode
this.inQty=null
this.outQty=(-varianceQty)?.toDouble()
this.stockOutLine=stockOutLine
this.balance=newBalance
this.type="Adj"
this.date = LocalDate.now()
}
stockLedgerRepository.save(stockLedger)
} }
if (varianceQty > BigDecimal.ZERO ) { if (varianceQty > BigDecimal.ZERO ) {
val stockIn=StockIn().apply {
this.code=stockTake.code
this.status="completed"
this.stockTake=stockTake
}
stockInRepository.save(stockIn)
var stockIn = stockInRepository.findByStockTakeIdAndDeletedFalse(stockTakeId)
?: StockIn().apply {
this.code = stockTake.code
this.status = "completed"
this.stockTake = stockTake
}.also { stockInRepository.save(it) }

val stockInLine = StockInLine().apply { val stockInLine = StockInLine().apply {
this.stockTakeLine=stockTakeLine this.stockTakeLine=stockTakeLine
this.item=inventoryLot.item this.item=inventoryLot.item
this.itemNo=stockTakeRecord.itemCode this.itemNo=stockTakeRecord.itemCode
this.stockIn = stockIn this.stockIn = stockIn
this.demandQty=finalQty
this.acceptedQty=finalQty
this.demandQty=varianceQty
this.acceptedQty=varianceQty
this.expiryDate=inventoryLot.expiryDate this.expiryDate=inventoryLot.expiryDate
this.inventoryLot=inventoryLot this.inventoryLot=inventoryLot
this.inventoryLotLine=inventoryLotLine this.inventoryLotLine=inventoryLotLine
@@ -891,9 +938,21 @@ open fun saveApproverStockTakeRecord(
} }
stockInLineRepository.save(stockInLine) stockInLineRepository.save(stockInLine)
val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()+varianceQty.toDouble()
val stockLedger = StockLedger().apply {
this.inventory=inventory
this.itemId=inventoryLot.item?.id
this.itemCode=inventoryLot.item?.code
this.inQty=varianceQty?.toDouble()
this.stockInLine=stockInLine
this.outQty=null
this.balance=newBalance
this.type="Adj"
this.date = LocalDate.now()
}
stockLedgerRepository.save(stockLedger)
} }
// val currentInQty = inventoryLotLine.inQty ?: BigDecimal.ZERO
// val newInQty = currentInQty.add(variance)

val updateRequest = SaveInventoryLotLineRequest( val updateRequest = SaveInventoryLotLineRequest(
id = inventoryLotLine.id, id = inventoryLotLine.id,
@@ -925,9 +984,7 @@ open fun batchSaveApproverStockTakeRecords(
val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId)
?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") ?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}")
// 2. Get all stock take records for this section where:
// - pickerFirstStockTakeQty is not 0
// - approverStockTakeQty is null (not yet approved)

val stockTakeRecords = stockTakeRecordRepository.findAll() val stockTakeRecords = stockTakeRecordRepository.findAll()
.filter { .filter {
!it.deleted && !it.deleted &&
@@ -982,7 +1039,7 @@ open fun batchSaveApproverStockTakeRecords(
this.approverName = user?.name this.approverName = user?.name
this.approverStockTakeQty = qty this.approverStockTakeQty = qty
this.approverBadQty = badQty this.approverBadQty = badQty
this.varianceQty = varianceQty // 应该是 0
this.varianceQty = varianceQty
this.status = "completed" this.status = "completed"
} }
@@ -1023,13 +1080,18 @@ open fun updateStockTakeRecordStatusToNotMatch(stockTakeRecordId: Long): StockTa
return stockTakeRecordRepository.save(stockTakeRecord) return stockTakeRecordRepository.save(stockTakeRecord)
} }


open fun getInventoryLotDetailsByStockTakeSectionNotMatch(stockTakeSection: String, stockTakeId: Long? = null): List<InventoryLotDetailResponse> {
println("getInventoryLotDetailsByStockTakeSectionNotMatch called with section: $stockTakeSection, stockTakeId: $stockTakeId")
open fun getInventoryLotDetailsByStockTakeSectionNotMatch(
stockTakeSection: String,
stockTakeId: Long? = null,
pageNum: Int = 0,
pageSize: Int = Int.MAX_VALUE // Default to return all if not specified
): RecordsRes<InventoryLotDetailResponse> {
println("getInventoryLotDetailsByStockTakeSectionNotMatch called with section: $stockTakeSection, stockTakeId: $stockTakeId, pageNum: $pageNum, pageSize: $pageSize")
val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection) val warehouses = warehouseRepository.findAllByStockTakeSectionAndDeletedIsFalse(stockTakeSection)
if (warehouses.isEmpty()) { if (warehouses.isEmpty()) {
logger.warn("No warehouses found for stockTakeSection: $stockTakeSection") logger.warn("No warehouses found for stockTakeSection: $stockTakeSection")
return emptyList()
return RecordsRes(emptyList(), 0)
} }
val warehouseIds = warehouses.mapNotNull { it.id } val warehouseIds = warehouses.mapNotNull { it.id }
@@ -1055,7 +1117,7 @@ open fun getInventoryLotDetailsByStockTakeSectionNotMatch(stockTakeSection: Stri
} }
// 只返回有 notMatch 记录的 inventory lot lines // 只返回有 notMatch 记录的 inventory lot lines
return inventoryLotLines.mapNotNull { ill ->
val allResults = inventoryLotLines.mapNotNull { ill ->
val inventoryLot = ill.inventoryLot val inventoryLot = ill.inventoryLot
val item = inventoryLot?.item val item = inventoryLot?.item
val warehouse = ill.warehouse val warehouse = ill.warehouse
@@ -1115,5 +1177,120 @@ open fun getInventoryLotDetailsByStockTakeSectionNotMatch(stockTakeSection: Stri
finalQty = stockTakeLine?.finalQty, finalQty = stockTakeLine?.finalQty,
) )
} }
// Apply pagination - if pageSize is Int.MAX_VALUE, return all results
val paginatedResult = if (pageSize >= allResults.size) {
// Return all results if pageSize is large enough
allResults
} else {
val pageable = PageRequest.of(pageNum, pageSize)
val startIndex = pageable.offset.toInt()
val endIndex = minOf(startIndex + pageSize, allResults.size)
if (startIndex < allResults.size) {
allResults.subList(startIndex, endIndex)
} else {
emptyList()
}
}
return RecordsRes(paginatedResult, allResults.size)
}



fun searchStockTransactions(request: SearchStockTransactionRequest): RecordsRes<StockTransactionResponse> {
val startTime = System.currentTimeMillis()
// 添加调试日志
println("Search request received: itemCode=${request.itemCode}, itemName=${request.itemName}, type=${request.type}, startDate=${request.startDate}, endDate=${request.endDate}")
// 验证:itemCode 或 itemName 至少一个不为 null 或空字符串
val itemCode = request.itemCode?.trim()?.takeIf { it.isNotEmpty() }
val itemName = request.itemName?.trim()?.takeIf { it.isNotEmpty() }
if (itemCode == null && itemName == null) {
println("Search validation failed: both itemCode and itemName are null/empty")
return RecordsRes(emptyList(), 0)
}
// request.startDate 和 request.endDate 已经是 LocalDate? 类型,不需要转换
val startDate = request.startDate
val endDate = request.endDate
println("Processed params: itemCode=$itemCode, itemName=$itemName, startDate=$startDate, endDate=$endDate")
// 使用 Repository 查询(更简单、更快)
val total = stockLedgerRepository.countStockTransactions(
itemCode = itemCode,
itemName = itemName,
type = request.type,
startDate = startDate,
endDate = endDate
)
println("Total count: $total")
// 如果 pageSize 是默认值(100)或未设置,使用 total 作为 pageSize
val actualPageSize = if (request.pageSize == 100) {
total.toInt().coerceAtLeast(1)
} else {
request.pageSize
}
// 计算 offset
val offset = request.pageNum * actualPageSize
// 查询所有符合条件的记录
val ledgers = stockLedgerRepository.findStockTransactions(
itemCode = itemCode,
itemName = itemName,
type = request.type,
startDate = startDate,
endDate = endDate
)
println("Found ${ledgers.size} ledgers")
val transactions = ledgers.map { ledger ->
val stockInLine = ledger.stockInLine
val stockOutLine = ledger.stockOutLine
StockTransactionResponse(
id = stockInLine?.id ?: stockOutLine?.id ?: 0L,
transactionType = if (ledger.inQty != null && ledger.inQty!! > 0) "IN" else "OUT",
itemId = ledger.itemId ?: 0L,
itemCode = ledger.itemCode,
itemName = ledger.inventory?.item?.name,
balanceQty = ledger.balance?.let { balance: Double -> BigDecimal(balance.toString()) },
qty = if (ledger.inQty != null && ledger.inQty!! > 0) {
BigDecimal(ledger.inQty.toString())
} else {
BigDecimal(ledger.outQty?.toString() ?: "0")
},
type = ledger.type,
status = stockInLine?.status ?: stockOutLine?.status ?: "",
transactionDate = ledger.created,
date = ledger.date,
lotNo = stockInLine?.lotNo ?: (stockOutLine?.inventoryLotLine?.inventoryLot?.lotNo),
stockInId = stockInLine?.stockIn?.id,
stockOutId = stockOutLine?.stockOut?.id,
remarks = stockInLine?.remarks
)
}
// 按 date 排序(从旧到新),如果 date 为 null 则使用 transactionDate 的日期部分
val sortedTransactions = transactions.sortedWith(
compareBy<StockTransactionResponse>(
{ it.date ?: it.transactionDate?.toLocalDate() },
{ it.transactionDate }
)
)
// 应用分页
val paginatedTransactions = sortedTransactions.drop(offset).take(actualPageSize)
val totalTime = System.currentTimeMillis() - startTime
println("Total time (Repository query): ${totalTime}ms, count: ${paginatedTransactions.size}, total: $total")
return RecordsRes(paginatedTransactions, total.toInt())
}
} }
}

+ 2
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt Wyświetl plik

@@ -480,6 +480,7 @@ open class SuggestedPickLotService(
this.qty = (suggestion.qty ?: BigDecimal.ZERO).toDouble() this.qty = (suggestion.qty ?: BigDecimal.ZERO).toDouble()
this.status = StockOutLineStatus.PENDING.status this.status = StockOutLineStatus.PENDING.status
this.deleted = false this.deleted = false
this.type = "Nor"
} }
val savedStockOutLine = stockOutLIneRepository.save(stockOutLine) val savedStockOutLine = stockOutLIneRepository.save(stockOutLine)
@@ -529,6 +530,7 @@ open class SuggestedPickLotService(
this.inventoryLotLine = suggestedLotLine this.inventoryLotLine = suggestedLotLine
this.pickOrderLine = updatedPickOrderLine this.pickOrderLine = updatedPickOrderLine
this.status = StockOutLineStatus.PENDING.status this.status = StockOutLineStatus.PENDING.status
this.type = "Nor"
} }
val savedStockOutLine = stockOutLIneRepository.saveAndFlush(stockOutLine) val savedStockOutLine = stockOutLIneRepository.saveAndFlush(stockOutLine)


+ 45
- 10
src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt Wyświetl plik

@@ -10,7 +10,8 @@ import org.springframework.web.bind.annotation.RequestBody
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import com.ffii.fpsms.modules.stock.web.model.* import com.ffii.fpsms.modules.stock.web.model.*
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory

import com.ffii.core.response.RecordsRes
import java.time.LocalDate
@RestController @RestController
@RequestMapping("/stockTakeRecord") @RequestMapping("/stockTakeRecord")
class StockTakeRecordController( class StockTakeRecordController(
@@ -29,9 +30,18 @@ class StockTakeRecordController(
@GetMapping("/inventoryLotDetailsBySectionNotMatch") @GetMapping("/inventoryLotDetailsBySectionNotMatch")
fun getInventoryLotDetailsByStockTakeSectionNotMatch( fun getInventoryLotDetailsByStockTakeSectionNotMatch(
@RequestParam stockTakeSection: String, @RequestParam stockTakeSection: String,
@RequestParam(required = false) stockTakeId: Long?
): List<InventoryLotDetailResponse> {
return stockOutRecordService.getInventoryLotDetailsByStockTakeSectionNotMatch(stockTakeSection, stockTakeId)
@RequestParam(required = false) stockTakeId: Long?,
@RequestParam(required = false, defaultValue = "0") pageNum: Int,
@RequestParam(required = false) pageSize: Int?
): RecordsRes<InventoryLotDetailResponse> {
// If pageSize is null, use a large number to return all records
val actualPageSize = pageSize ?: Int.MAX_VALUE
return stockOutRecordService.getInventoryLotDetailsByStockTakeSectionNotMatch(
stockTakeSection,
stockTakeId,
pageNum,
actualPageSize
)
} }
@GetMapping("/inventoryLotDetails") @GetMapping("/inventoryLotDetails")
@@ -42,12 +52,14 @@ class StockTakeRecordController(
} }
@GetMapping("/inventoryLotDetailsBySection") @GetMapping("/inventoryLotDetailsBySection")
fun getInventoryLotDetailsByStockTakeSection(
@RequestParam stockTakeSection: String,
@RequestParam(required = false) stockTakeId: Long?
): List<InventoryLotDetailResponse> {
return stockOutRecordService.getInventoryLotDetailsByStockTakeSection(stockTakeSection, stockTakeId)
}
fun getInventoryLotDetailsByStockTakeSection(
@RequestParam stockTakeSection: String,
@RequestParam(required = false) stockTakeId: Long?,
@RequestParam(required = false, defaultValue = "0") pageNum: Int,
@RequestParam(required = false, defaultValue = "10") pageSize: Int
): RecordsRes<InventoryLotDetailResponse> {
return stockOutRecordService.getInventoryLotDetailsByStockTakeSection(stockTakeSection, stockTakeId, pageNum, pageSize)
}
@PostMapping("/saveStockTakeRecord") @PostMapping("/saveStockTakeRecord")
fun saveStockTakeRecord( fun saveStockTakeRecord(
@@ -177,4 +189,27 @@ class StockTakeRecordController(
)) ))
} }
} }


@GetMapping("/searchStockTransactions")
fun searchStockTransactions(
@RequestParam(required = false) itemCode: String?,
@RequestParam(required = false) itemName: String?,
@RequestParam(required = false) type: String?,
@RequestParam(required = false) startDate: LocalDate?,
@RequestParam(required = false) endDate: LocalDate?,
@RequestParam(required = false, defaultValue = "0") pageNum: Int,
@RequestParam(required = false, defaultValue = "100") pageSize: Int
): RecordsRes<StockTransactionResponse> {
val request = SearchStockTransactionRequest(
startDate = startDate,
endDate = endDate,
itemCode = itemCode,
itemName = itemName,
type = type,
pageNum = pageNum,
pageSize = pageSize
)
return stockOutRecordService.searchStockTransactions(request)
}
} }

+ 16
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockInRequest.kt Wyświetl plik

@@ -70,3 +70,19 @@ data class SaveInventoryLotLineForSil (
val qty: BigDecimal, val qty: BigDecimal,
val warehouseId: Long? val warehouseId: Long?
) )
data class NewStockInCreateRequest(
val inventoryLotLineId: Long,
val itemId: Long,
val itemNo: String,

val demandQty: BigDecimal,
val acceptedQty: BigDecimal,
val expiryDate: LocalDate,
val lotNo: String?,
val stockIntype: String,
val stockInLineType: String,

val warehouseId: Long,
val stockUomId: Long,
)

+ 32
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt Wyświetl plik

@@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonFormat
import java.time.LocalDate import java.time.LocalDate
import java.math.BigDecimal import java.math.BigDecimal
import java.time.LocalDateTime import java.time.LocalDateTime
import com.ffii.core.response.RecordsRes

data class AllPickedStockTakeListReponse( data class AllPickedStockTakeListReponse(
val id: Long, val id: Long,
val stockTakeSession: String, val stockTakeSession: String,
@@ -98,4 +100,34 @@ data class BatchSaveApproverStockTakeRecordResponse(
val successCount: Int, val successCount: Int,
val errorCount: Int, val errorCount: Int,
val errors: List<String> val errors: List<String>
)
data class SearchStockTransactionRequest(
val startDate: LocalDate? = null,
val endDate: LocalDate? = null,
val itemCode: String? = null,
val itemName: String? = null,
val type: String? = null,
val pageNum: Int = 0,
val pageSize: Int = 100
)
data class StockTransactionResponse(
val id: Long,
val transactionType: String,
val itemId: Long,
val itemCode: String?,
val itemName: String?,
val balanceQty: BigDecimal? = null,
val qty: BigDecimal,
val type: String?,
val date: LocalDate?,
val status: String,
val transactionDate: LocalDateTime?,
val lotNo: String?,
val stockInId: Long?,
val stockOutId: Long?,
val remarks: String?
)

data class StockTransactionListResponse(
val records: RecordsRes<StockTransactionResponse>
) )

+ 7
- 0
src/main/resources/db/changelog/changes/20260109_enson/02_alter_table.sql Wyświetl plik

@@ -0,0 +1,7 @@
--liquibase formatted sql
--changeset author:add_time_fields_to_productprocessline

ALTER TABLE `stock_out_line`
ADD COLUMN `startTime` DATETIME NULL AFTER `type`,
ADD COLUMN `endTime` DATETIME NULL AFTER `startTime`;


+ 8
- 0
src/main/resources/db/changelog/changes/20260112_01_Enson/01_alter_table.sql Wyświetl plik

@@ -0,0 +1,8 @@
--liquibase formatted sql
--changeset author:add_type_and_item_id_to_stock_ledger

ALTER TABLE `stock_ledger`
ADD COLUMN `type` varchar(255) NULL AFTER `balance`,
ADD COLUMN `itemId` INT NULL AFTER `inventoryId`,
ADD COLUMN `itemCode` varchar(255) NULL AFTER `itemId`;


+ 7
- 0
src/main/resources/db/changelog/changes/20260112_01_Enson/02_alter_table.sql Wyświetl plik

@@ -0,0 +1,7 @@
--liquibase formatted sql
--changeset author:add_type_and_item_id_to_stock_ledger

ALTER TABLE `stock_ledger`
ADD COLUMN `date` varchar(255) NULL AFTER `deleted`;



+ 7
- 0
src/main/resources/db/changelog/changes/20260112_01_Enson/03_alter_table.sql Wyświetl plik

@@ -0,0 +1,7 @@
--liquibase formatted sql
--changeset author:add_type_and_item_id_to_stock_ledger

ALTER TABLE `stock_out`
ADD COLUMN `stockTakeId` INT NULL AFTER `remarks`;



Ładowanie…
Anuluj
Zapisz