Просмотр исходного кода

排產 Excel

在「物料用量總結」加入「需採購/生產數量」=下週用量 − 庫存(不扣 PO,可為負)
欄寬加寬;不放在每日分頁
工單
列表顯示 BOM 工序標準工時(預估分鐘)
「BOM材料」→「材料」
耗材「不適用」→「不用提料」
BOM 工藝
新增 bom.details(含 Liquibase)
「工藝明細」改為可儲存的自由文字;「工藝流程」仍用名稱
看板 / 分頁
工單狀態看板經手人:無 handler 時回退顯示 operator
隱藏飲料生產數量分頁
排產規則文案
「一般日」→「一般規則」
「多備日」等「多備」相關 →「特別規則」
「多備星期」→「特別規則適用星期」(前後端同步)
debrand
tommy 2 дней назад
Родитель
Сommit
81378f6b8a
10 измененных файлов: 79 добавлений и 10 удалений
  1. +3
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/entity/projections/JobOrderInfo.kt
  2. +16
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt
  3. +3
    -3
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/PsScheduleRuleService.kt
  4. +4
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/Bom.kt
  5. +18
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/BomProcessRepository.kt
  6. +13
    -0
      src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt
  7. +6
    -4
      src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt
  8. +10
    -0
      src/main/java/com/ffii/fpsms/modules/master/web/BomController.kt
  9. +2
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt
  10. +4
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt

+ 3
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/entity/projections/JobOrderInfo.kt Просмотреть файл

@@ -68,7 +68,9 @@ data class JobOrderInfoWithTypeName(
val status: String, val status: String,
val jobTypeId: Long?, val jobTypeId: Long?,
val jobTypeName: String?, val jobTypeName: String?,
val lotNo: String?
val lotNo: String?,
/** Standard BOM process minutes (prep + duration + post). Null if unknown. */
val estimatedMinutes: Int? = null,
) )
// Job Order // Job Order
interface JobOrderDetailWithJsonString { interface JobOrderDetailWithJsonString {


+ 16
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt Просмотреть файл

@@ -69,6 +69,7 @@ import java.math.RoundingMode
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import com.ffii.fpsms.modules.master.entity.BomMaterialRepository import com.ffii.fpsms.modules.master.entity.BomMaterialRepository
import com.ffii.fpsms.modules.master.entity.BomProcessRepository
import com.ffii.fpsms.modules.master.service.ItemUomService import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.master.web.models.ConvertUomByItemRequest import com.ffii.fpsms.modules.master.web.models.ConvertUomByItemRequest
import com.ffii.fpsms.modules.stock.service.StockInLineService import com.ffii.fpsms.modules.stock.service.StockInLineService
@@ -96,6 +97,7 @@ open class JobOrderService(
val productProcessRepository: ProductProcessRepository, val productProcessRepository: ProductProcessRepository,
val jobOrderBomMaterialRepository: JobOrderBomMaterialRepository, val jobOrderBomMaterialRepository: JobOrderBomMaterialRepository,
val bomMaterialRepository: BomMaterialRepository, val bomMaterialRepository: BomMaterialRepository,
val bomProcessRepository: BomProcessRepository,
val itemUomService: ItemUomService val itemUomService: ItemUomService


) { ) {
@@ -171,6 +173,15 @@ open class JobOrderService(
} else { } else {
emptyMap() emptyMap()
} }

val bomIds = jobOrdersMap.values.mapNotNull { it.bom?.id }.distinct()
val estimatedMinutesByBomId: Map<Long, Int> = if (bomIds.isNotEmpty()) {
bomProcessRepository.sumStandardMinutesByBomIds(bomIds).associate { agg ->
agg.bomId to agg.totalMinutes.toInt().coerceAtLeast(0)
}
} else {
emptyMap()
}
// 移除内存中的日期过滤,因为已经在数据库层面过滤了 // 移除内存中的日期过滤,因为已经在数据库层面过滤了
val records = response.content val records = response.content
@@ -184,6 +195,9 @@ open class JobOrderService(


Pair(null, null) Pair(null, null)
} }

val bomId = jobOrder?.bom?.id
val estimatedMinutes = bomId?.let { estimatedMinutesByBomId[it] }
JobOrderInfoWithTypeName( JobOrderInfoWithTypeName(
id = info.id, id = info.id,
@@ -210,7 +224,8 @@ open class JobOrderService(
}, },
jobTypeId = info.jobTypeId, jobTypeId = info.jobTypeId,
jobTypeName = info.jobTypeId?.let { jobTypes[it]?.name }, jobTypeName = info.jobTypeId?.let { jobTypes[it]?.name },
lotNo = info.lotNo
lotNo = info.lotNo,
estimatedMinutes = estimatedMinutes,
) )
} }
.filter { info -> .filter { info ->


+ 3
- 3
src/main/java/com/ffii/fpsms/modules/jobOrder/service/PsScheduleRuleService.kt Просмотреть файл

@@ -134,12 +134,12 @@ open class PsScheduleRuleService(
} }


fun savePlantRules(request: SavePsPlantRulesRequest) { fun savePlantRules(request: SavePsPlantRulesRequest) {
requireDays(request.redLineDays, request.safetyStockDays, UiText.t("一般日", "Normal days"))
requireDays(request.prepareMoreRedLineDays, request.prepareMoreSafetyStockDays, UiText.t("多備日", "Extra-prep days"))
requireDays(request.redLineDays, request.safetyStockDays, UiText.t("一般規則", "Normal rules"))
requireDays(request.prepareMoreRedLineDays, request.prepareMoreSafetyStockDays, UiText.t("特別規則", "Special rules"))
val coffee = requireProduceDays(request.coffeeDays, UiText.t("類別 A", "Category A")) val coffee = requireProduceDays(request.coffeeDays, UiText.t("類別 A", "Category A"))
val tea = requireProduceDays(request.teaDays, UiText.t("類別 B", "Category B")) val tea = requireProduceDays(request.teaDays, UiText.t("類別 B", "Category B"))
val lemon = requireProduceDays(request.lemonDays, UiText.t("類別 C", "Category C")) val lemon = requireProduceDays(request.lemonDays, UiText.t("類別 C", "Category C"))
val prepareMore = parseDaysStrict(request.prepareMoreDays, UiText.t("多備星期", "Extra-prep weekdays"))
val prepareMore = parseDaysStrict(request.prepareMoreDays, UiText.t("特別規則適用星期", "Special-rule weekdays"))
val noProduction = parseDaysStrict(request.noProductionDays, UiText.t("不生產星期", "Non-production weekdays")) val noProduction = parseDaysStrict(request.noProductionDays, UiText.t("不生產星期", "Non-production weekdays"))


upsert(RED_LINE, formatNumber(request.redLineDays), "float") upsert(RED_LINE, formatNumber(request.redLineDays), "float")


+ 4
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/Bom.kt Просмотреть файл

@@ -56,6 +56,10 @@ open class Bom : BaseEntity<Long>() {
@Column(name = "description", nullable = false, length = 100) @Column(name = "description", nullable = false, length = 100)
open var description: String? = null open var description: String? = null


/** Free-text process / craft details shown on job order 工藝明細. */
@Column(name = "details", columnDefinition = "TEXT")
open var details: String? = null

@Column(name = "outputQty", precision = 14, scale = 2) @Column(name = "outputQty", precision = 14, scale = 2)
open var outputQty: BigDecimal? = null open var outputQty: BigDecimal? = null




+ 18
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/BomProcessRepository.kt Просмотреть файл

@@ -30,4 +30,22 @@ interface BomProcessRepository : AbstractRepository<BomProcess, Long> {
""" """
) )
fun sumDurationByBomIds(@Param("bomIds") bomIds: Collection<Long>): List<BomDurationAggregate> fun sumDurationByBomIds(@Param("bomIds") bomIds: Collection<Long>): List<BomDurationAggregate>

/** prep + duration + post for JO list estimated time */
@Query(
"""
SELECT
bp.bom.id AS bomId,
COALESCE(SUM(
COALESCE(bp.prepTimeInMinute, 0) +
COALESCE(bp.durationInMinute, 0) +
COALESCE(bp.postProdTimeInMinute, 0)
), 0) AS totalMinutes
FROM BomProcess bp
WHERE bp.deleted = false
AND bp.bom.id IN :bomIds
GROUP BY bp.bom.id
"""
)
fun sumStandardMinutesByBomIds(@Param("bomIds") bomIds: Collection<Long>): List<BomDurationAggregate>
} }

+ 13
- 0
src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt Просмотреть файл

@@ -212,6 +212,19 @@ open class BomService(
return response return response
} }


/** Update free-text craft details (工藝明細) on BOM. */
@Transactional
open fun updateBomDetails(id: Long, details: String?): Map<String, Any?> {
val bom = bomRepository.findByIdAndDeletedIsFalse(id)
?: throw NotFoundException()
bom.details = details
bomRepository.saveAndFlush(bom)
return mapOf(
"id" to bom.id,
"details" to bom.details,
)
}

/** /**
* Edit BOM (basic fields + BOM materials + BOM process lines). * Edit BOM (basic fields + BOM materials + BOM process lines).
* *


+ 6
- 4
src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt Просмотреть файл

@@ -1564,7 +1564,8 @@ open class ProductionScheduleService(
val matSheet = workbook.createSheet(UiText.t("物料用量總結", "Material usage summary")) val matSheet = workbook.createSheet(UiText.t("物料用量總結", "Material usage summary"))


val matHeaders = listOf( val matHeaders = listOf(
UiText.t("物料編號", "Material code"), UiText.t("物料名稱", "Material name"), UiText.t("需求量", "Required qty"), UiText.t("總用量", "Total usage"),
UiText.t("物料編號", "Material code"), UiText.t("物料名稱", "Material name"),
UiText.t("需採購/生產數量", "Qty to purchase/produce"), UiText.t("總用量", "Total usage"),
UiText.t("單位", "UOM"), UiText.t("數量(採購單位)", "Qty (purchase UOM)"), UiText.t("採購單位", "Purchase UOM"), UiText.t("單位", "UOM"), UiText.t("數量(採購單位)", "Qty (purchase UOM)"), UiText.t("採購單位", "Purchase UOM"),
UiText.t("已採購數量", "Purchased qty"), UiText.t("庫存數", "On hand"), UiText.t("庫存單位", "Stock UOM"), UiText.t("已採購數量", "Purchased qty"), UiText.t("庫存數", "On hand"), UiText.t("庫存單位", "Stock UOM"),
UiText.t("相關成品編號", "Related finished-good code"), UiText.t("相關成品名稱", "Related finished-good name") UiText.t("相關成品編號", "Related finished-good code"), UiText.t("相關成品名稱", "Related finished-good name")
@@ -1586,12 +1587,13 @@ open class ProductionScheduleService(
val purchaseQtyNeed = asDouble(rowData["purchaseQtyNeed"]) val purchaseQtyNeed = asDouble(rowData["purchaseQtyNeed"])
val purchased = asDouble(rowData["purchasedQty"]) val purchased = asDouble(rowData["purchasedQty"])
val onHand = asDouble(rowData["onHandQty"]) val onHand = asDouble(rowData["onHandQty"])
val requiredQty = (totalNeed - purchased - onHand).coerceAtLeast(0.0)
// 總用量 − 庫存;不扣在途採購。負數代表有餘量。
val qtyToPurchaseOrProduce = totalNeed - onHand


val values = listOf<Any>( val values = listOf<Any>(
rowData["matCode"]?.toString() ?: "", rowData["matCode"]?.toString() ?: "",
rowData["matName"]?.toString() ?: "", rowData["matName"]?.toString() ?: "",
requiredQty,
qtyToPurchaseOrProduce,
totalNeed, totalNeed,
rowData["uomName"]?.toString() ?: "", rowData["uomName"]?.toString() ?: "",
purchaseQtyNeed, purchaseQtyNeed,
@@ -1628,7 +1630,7 @@ open class ProductionScheduleService(
// Column widths for material summary // Column widths for material summary
matSheet.setColumnWidth(0, 20 * 256) matSheet.setColumnWidth(0, 20 * 256)
matSheet.setColumnWidth(1, 50 * 256) // 物料名稱 matSheet.setColumnWidth(1, 50 * 256) // 物料名稱
matSheet.setColumnWidth(2, 16 * 256)
matSheet.setColumnWidth(2, 22 * 256) // 需採購/生產數量
matSheet.setColumnWidth(3, 16 * 256) matSheet.setColumnWidth(3, 16 * 256)
matSheet.setColumnWidth(4, 12 * 256) matSheet.setColumnWidth(4, 12 * 256)
matSheet.setColumnWidth(5, 16 * 256) matSheet.setColumnWidth(5, 16 * 256)


+ 10
- 0
src/main/java/com/ffii/fpsms/modules/master/web/BomController.kt Просмотреть файл

@@ -29,6 +29,7 @@ import com.ffii.fpsms.modules.master.web.models.BomFormatCheckRequest
import com.ffii.fpsms.modules.master.web.models.ImportBomRequestPayload import com.ffii.fpsms.modules.master.web.models.ImportBomRequestPayload
import com.ffii.fpsms.modules.master.web.models.BomDetailResponse import com.ffii.fpsms.modules.master.web.models.BomDetailResponse
import com.ffii.fpsms.modules.master.web.models.EditBomRequest import com.ffii.fpsms.modules.master.web.models.EditBomRequest
import com.ffii.fpsms.modules.master.web.models.UpdateBomDetailsRequest
import com.ffii.fpsms.modules.master.web.models.BomExcelCheckProgress import com.ffii.fpsms.modules.master.web.models.BomExcelCheckProgress
import com.ffii.fpsms.modules.master.web.models.BomIdByItemCodeResponse import com.ffii.fpsms.modules.master.web.models.BomIdByItemCodeResponse
import com.ffii.fpsms.modules.master.web.models.MasterDataIssueResponse import com.ffii.fpsms.modules.master.web.models.MasterDataIssueResponse
@@ -169,4 +170,13 @@ fun getBomDetail(@PathVariable id: Long): BomDetailResponse {
): BomDetailResponse { ): BomDetailResponse {
return bomService.editBom(id, request) return bomService.editBom(id, request)
} }

/** Update free-text craft details (工藝明細) only. */
@PutMapping("/{id}/details")
fun updateBomDetails(
@PathVariable id: Long,
@RequestBody request: UpdateBomDetailsRequest,
): Map<String, Any?> {
return bomService.updateBomDetails(id, request.details)
}
} }

+ 2
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt Просмотреть файл

@@ -24,6 +24,8 @@ data class ProductProcessInfo(
val jobOrderStatus: String?, val jobOrderStatus: String?,
val jobType: String?, val jobType: String?,
val bomDescription: String?, val bomDescription: String?,
/** Free-text craft details from bom.details */
val bomDetails: String? = null,
val isDark: String?, val isDark: String?,
val isDense: Int?, val isDense: Int?,
val isFloat: String?, val isFloat: String?,


+ 4
- 1
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt Просмотреть файл

@@ -674,6 +674,7 @@ open class ProductProcessService(
jobOrderStatus = jobOrder?.status?.value ?: "", jobOrderStatus = jobOrder?.status?.value ?: "",
jobType = jobType?.name ?: "", jobType = jobType?.name ?: "",
bomDescription = bom?.description ?: "", bomDescription = bom?.description ?: "",
bomDetails = bom?.details,
itemId = bom?.item?.id ?: 0, itemId = bom?.item?.id ?: 0,
itemCode = bom?.item?.code ?: "", itemCode = bom?.item?.code ?: "",
itemName = bom?.item?.name ?: "", itemName = bom?.item?.name ?: "",
@@ -2602,7 +2603,9 @@ open class ProductProcessService(
processName = line.name, processName = line.name,
equipmentName = equipmentName, equipmentName = equipmentName,
equipmentDetailName = equipmentDetailName, equipmentDetailName = equipmentDetailName,
handlerName = line.handler?.name,
// Scan staff writes operator; startLine writes handler. Prefer handler, fall back to operator.
handlerName = line.handler?.name?.takeIf { it.isNotBlank() }
?: line.operator?.name,
startTime = line.startTime, startTime = line.startTime,
endTime = line.endTime, endTime = line.endTime,
processingTime = line.processingTime, processingTime = line.processingTime,


Загрузка…
Отмена
Сохранить