Kaynağa Gözat

no message

master
Fai Luk 3 gün önce
ebeveyn
işleme
1981b3fcf3
7 değiştirilmiş dosya ile 138 ekleme ve 6 silme
  1. +14
    -1
      src/main/java/com/ffii/fpsms/m18/M18GrnRules.kt
  2. +61
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt
  3. +10
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt
  4. +6
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/PlasticPrintRequest.kt
  5. +18
    -3
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt
  6. +5
    -0
      src/main/resources/application.yml
  7. +24
    -2
      src/main/resources/jasper/StockInTraceabilityReport.jrxml

+ 14
- 1
src/main/java/com/ffii/fpsms/m18/M18GrnRules.kt Dosyayı Görüntüle

@@ -3,7 +3,20 @@ package com.ffii.fpsms.m18
/** /**
* M18 PO [createUid] is stored on [com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder.m18CreatedUId]. * M18 PO [createUid] is stored on [com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder.m18CreatedUId].
* For these M18 user ids, FPSMS does not post GRN to M18 (see [com.ffii.fpsms.modules.stock.service.StockInLineService]). * For these M18 user ids, FPSMS does not post GRN to M18 (see [com.ffii.fpsms.modules.stock.service.StockInLineService]).
* Remarks are shown on PO stock-in traceability report ([com.ffii.fpsms.modules.report.service.ReportService.searchStockInTraceabilityReport]).
*/ */
object M18GrnRules { object M18GrnRules {
val SKIP_GRN_FOR_M18_CREATED_UIDS: Set<Long> = setOf(2569L, 2676L)
private val REMARKS_FOR_M18_CREATED_UID: Map<Long, String> = mapOf(
2569L to "legato",
2676L to "xtech",
)

val SKIP_GRN_FOR_M18_CREATED_UIDS: Set<Long> = REMARKS_FOR_M18_CREATED_UID.keys

/** Display string for PDF/Excel: `2569 (legato)`, or plain id when unknown. */
fun formatM18CreatedUidForReport(uid: Long?): String {
if (uid == null) return ""
val remark = REMARKS_FOR_M18_CREATED_UID[uid]
return if (remark != null) "$uid ($remark)" else uid.toString()
}
} }

+ 61
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt Dosyayı Görüntüle

@@ -9,12 +9,14 @@ import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendResponse
import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SettingsResponse import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SettingsResponse
import com.ffii.fpsms.modules.jobOrder.web.model.PrintRequest import com.ffii.fpsms.modules.jobOrder.web.model.PrintRequest
import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest
import com.ffii.fpsms.modules.jobOrder.web.model.NgpclPushResponse
import com.ffii.fpsms.modules.jobOrder.web.model.OnPackQrJobOrderRequest import com.ffii.fpsms.modules.jobOrder.web.model.OnPackQrJobOrderRequest
import com.ffii.fpsms.modules.settings.service.SettingsService import com.ffii.fpsms.modules.settings.service.SettingsService
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.py.PrintedQtyByChannel import com.ffii.fpsms.py.PrintedQtyByChannel
import com.ffii.fpsms.py.PyJobOrderListItem import com.ffii.fpsms.py.PyJobOrderListItem
import com.ffii.fpsms.py.PyJobOrderPrintSubmitService import com.ffii.fpsms.py.PyJobOrderPrintSubmitService
import org.springframework.core.env.Environment
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.awt.Color import java.awt.Color
import java.awt.Font import java.awt.Font
@@ -28,6 +30,10 @@ import javax.imageio.ImageIO
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.QRCodeWriter
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.net.Socket import java.net.Socket
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.io.PrintWriter import java.io.PrintWriter
@@ -41,6 +47,7 @@ import java.net.ConnectException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import org.springframework.core.io.ClassPathResource import org.springframework.core.io.ClassPathResource
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.time.Duration
import java.time.LocalDate import java.time.LocalDate


// Data class to store bitmap bytes + width (for XML) // Data class to store bitmap bytes + width (for XML)
@@ -53,6 +60,7 @@ class PlasticBagPrinterService(
private val stockInLineRepository: StockInLineRepository, private val stockInLineRepository: StockInLineRepository,
private val settingsService: SettingsService, private val settingsService: SettingsService,
private val pyJobOrderPrintSubmitService: PyJobOrderPrintSubmitService, private val pyJobOrderPrintSubmitService: PyJobOrderPrintSubmitService,
private val environment: Environment,
) { ) {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)


@@ -537,6 +545,59 @@ class PlasticBagPrinterService(
return baos.toByteArray() return baos.toByteArray()
} }


/**
* Builds the same ZIP as [generateOnPackQrTextZip] and POSTs it to [ngpcl.push-url] (application/zip).
* When the URL is blank, returns [NgpclPushResponse] with pushed=false so callers can fall back to manual download.
*/
fun pushOnPackQrTextZipToNgpcl(jobOrders: List<OnPackQrJobOrderRequest>): NgpclPushResponse {
val url = (environment.getProperty("ngpcl.push-url") ?: "").trim()
if (url.isEmpty()) {
return NgpclPushResponse(
pushed = false,
message = "NGPCL push URL not configured. Set ngpcl.push-url or NGPCL_PUSH_URL, or download the ZIP and transfer loose files manually.",
)
}
val zipBytes = try {
generateOnPackQrTextZip(jobOrders)
} catch (e: Exception) {
logger.warn("OnPack text ZIP generation failed before NGPCL push", e)
return NgpclPushResponse(
pushed = false,
message = e.message ?: "ZIP generation failed",
)
}
return try {
val client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30))
.build()
val request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofMinutes(2))
.header("Content-Type", "application/zip")
.POST(HttpRequest.BodyPublishers.ofByteArray(zipBytes))
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
val body = response.body()
if (response.statusCode() in 200..299) {
NgpclPushResponse(
pushed = true,
message = "NGPCL accepted (HTTP ${response.statusCode()})${if (body.isNotBlank()) ": ${body.take(300)}" else ""}",
)
} else {
NgpclPushResponse(
pushed = false,
message = "NGPCL returned HTTP ${response.statusCode()}: ${body.take(500)}",
)
}
} catch (e: Exception) {
logger.error("NGPCL push failed", e)
NgpclPushResponse(
pushed = false,
message = e.message ?: "Push failed: ${e.javaClass.simpleName}",
)
}
}

/** /**
* Returns uppercase item codes present in `onpack_qr` with the given [templateType] (`bmp` or `text`). * Returns uppercase item codes present in `onpack_qr` with the given [templateType] (`bmp` or `text`).
* Empty or NULL `template_type` is treated as `bmp` for backward compatibility. * Empty or NULL `template_type` is treated as `bmp` for backward compatibility.


+ 10
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt Dosyayı Görüntüle

@@ -7,6 +7,7 @@ import com.ffii.fpsms.modules.jobOrder.web.model.Laser2Request
import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendRequest import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendRequest
import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendResponse import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendResponse
import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SettingsResponse import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SettingsResponse
import com.ffii.fpsms.modules.jobOrder.web.model.NgpclPushResponse
import com.ffii.fpsms.modules.jobOrder.web.model.OnPackQrDownloadRequest import com.ffii.fpsms.modules.jobOrder.web.model.OnPackQrDownloadRequest
import com.ffii.fpsms.modules.jobOrder.web.model.PrinterStatusRequest import com.ffii.fpsms.modules.jobOrder.web.model.PrinterStatusRequest
import com.ffii.fpsms.modules.jobOrder.web.model.PrinterStatusResponse import com.ffii.fpsms.modules.jobOrder.web.model.PrinterStatusResponse
@@ -152,6 +153,15 @@ class PlasticBagPrinterController(
} }
} }


/**
* Same payload as [downloadOnPackQrText], but POSTs the generated ZIP to `ngpcl.push-url` (application/zip).
* Returns JSON: `{ pushed, message }`. When push URL is unset, `pushed` is false — use download instead.
*/
@PostMapping("/ngpcl/push-onpack-qr-text")
fun pushOnPackQrTextToNgpcl(@RequestBody request: OnPackQrDownloadRequest): ResponseEntity<NgpclPushResponse> {
return ResponseEntity.ok(plasticBagPrinterService.pushOnPackQrTextZipToNgpcl(request.jobOrders))
}

/** /**
* Test API to generate and download the printer job files as a ZIP. * Test API to generate and download the printer job files as a ZIP.
* ONPACK2030 * ONPACK2030


+ 6
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/PlasticPrintRequest.kt Dosyayı Görüntüle

@@ -57,4 +57,10 @@ data class OnPackQrDownloadRequest(
data class OnPackQrJobOrderRequest( data class OnPackQrJobOrderRequest(
val jobOrderId: Long, val jobOrderId: Long,
val itemCode: String, val itemCode: String,
)

/** Result of POST /plastic/ngpcl/push-onpack-qr-text — server POSTs the lemon ZIP bytes to [ngpcl.push-url] when configured. */
data class NgpclPushResponse(
val pushed: Boolean,
val message: String,
) )

+ 18
- 3
src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt Dosyayı Görüntüle

@@ -647,9 +647,10 @@ return result
} }


/** /**
* Queries the database for Stock In Traceability Report data.
* Queries the database for Stock In Traceability Report data (PO 入倉 / 入倉追蹤 PDF).
* Joins stock_in_line, stock_in, items, qc_result, inventory_lot, inventory_lot_line, warehouse, and shop tables. * Joins stock_in_line, stock_in, items, qc_result, inventory_lot, inventory_lot_line, warehouse, and shop tables.
* Supports comma-separated values for stockCategory (items.type) and itemCode. * Supports comma-separated values for stockCategory (items.type) and itemCode.
* Adds `poM18CreatorDisplay` from `purchase_order.m18CreatedUId` via [M18GrnRules.formatM18CreatedUidForReport].
*/ */
fun searchStockInTraceabilityReport( fun searchStockInTraceabilityReport(
stockCategory: String?, stockCategory: String?,
@@ -704,6 +705,7 @@ return result
COALESCE(wh.code, '') as storeLocation, COALESCE(wh.code, '') as storeLocation,
COALESCE(sp_si.code, sp_po.code, '') as supplierID, COALESCE(sp_si.code, sp_po.code, '') as supplierID,
COALESCE(sp_si.name, sp_po.name, '') as supplierName, COALESCE(sp_si.name, sp_po.name, '') as supplierName,
po.m18CreatedUId AS poM18CreatedUId,
TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(SUM(COALESCE(sil.acceptedQty, 0)) OVER (PARTITION BY it.id), 2))) as totalStockInQty, TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(SUM(COALESCE(sil.acceptedQty, 0)) OVER (PARTITION BY it.id), 2))) as totalStockInQty,
TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(SUM(COALESCE(sil.acceptedQty, 0)) OVER (PARTITION BY it.id), 2))) as totalIqcSampleQty TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(SUM(COALESCE(sil.acceptedQty, 0)) OVER (PARTITION BY it.id), 2))) as totalIqcSampleQty
FROM stock_in_line sil FROM stock_in_line sil
@@ -737,8 +739,21 @@ return result
$lastInDateEndSql $lastInDateEndSql
ORDER BY it.code, sil.lotNo ORDER BY it.code, sil.lotNo
""".trimIndent() """.trimIndent()
return jdbcDao.queryForList(sql, args)

val rows = jdbcDao.queryForList(sql, args)
return rows.map { row ->
val m = LinkedHashMap<String, Any?>(row)
val raw = m.remove("poM18CreatedUId")
val uid = when (raw) {
null -> null
is Number -> raw.toLong()
is BigDecimal -> raw.toLong()
else -> raw.toString().toLongOrNull()
}
m["poM18CreatorDisplay"] = M18GrnRules.formatM18CreatedUidForReport(uid)
@Suppress("UNCHECKED_CAST")
m as Map<String, Any>
}
} }


/** /**


+ 5
- 0
src/main/resources/application.yml Dosyayı Görüntüle

@@ -50,6 +50,11 @@ jwt:
logging: logging:
config: 'classpath:log4j2.yml' config: 'classpath:log4j2.yml'


# Optional NGPCL gateway: receives the same bytes as /plastic/download-onpack-qr-text (Content-Type: application/zip).
# Leave empty to disable; set NGPCL_PUSH_URL in production if you expose an HTTP receiver for the lemon OnPack ZIP.
ngpcl:
push-url: ${NGPCL_PUSH_URL:}

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


+ 24
- 2
src/main/resources/jasper/StockInTraceabilityReport.jrxml Dosyayı Görüntüle

@@ -57,6 +57,7 @@
<field name="storeLocation" class="java.lang.String"/> <field name="storeLocation" class="java.lang.String"/>
<field name="supplierID" class="java.lang.String"/> <field name="supplierID" class="java.lang.String"/>
<field name="supplierName" class="java.lang.String"/> <field name="supplierName" class="java.lang.String"/>
<field name="poM18CreatorDisplay" class="java.lang.String"/>
<field name="totalStockInQty" class="java.lang.String"/> <field name="totalStockInQty" class="java.lang.String"/>
<field name="totalIqcSampleQty" class="java.lang.String"/> <field name="totalIqcSampleQty" class="java.lang.String"/>
<field name="totalIqcDefectQty" class="java.lang.String"/> <field name="totalIqcDefectQty" class="java.lang.String"/>
@@ -173,7 +174,7 @@
<text><![CDATA[送貨單編號]]></text> <text><![CDATA[送貨單編號]]></text>
</staticText> </staticText>
<staticText> <staticText>
<reportElement stretchType="RelativeToTallestObject" x="690" y="80" width="108" height="28" uuid="db5b9c55-0185-420b-ba6c-0e10d154cc8a">
<reportElement stretchType="RelativeToTallestObject" x="690" y="80" width="65" height="28" uuid="db5b9c55-0185-420b-ba6c-0e10d154cc8a">
<property name="com.jaspersoft.studio.unit.y" value="px"/> <property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/> <property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/> <property name="com.jaspersoft.studio.unit.width" value="px"/>
@@ -183,6 +184,18 @@
</textElement> </textElement>
<text><![CDATA[供應商名稱]]></text> <text><![CDATA[供應商名稱]]></text>
</staticText> </staticText>
<staticText>
<reportElement stretchType="RelativeToTallestObject" x="758" y="80" width="42" height="28" uuid="a1b2c3d4-e5f6-7890-abcd-ef1234567890">
<property name="com.jaspersoft.studio.unit.y" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="9"/>
</textElement>
<text><![CDATA[PO建立者
(M18)]]></text>
</staticText>
<staticText> <staticText>
<reportElement stretchType="RelativeToTallestObject" x="220" y="80" width="60" height="28" uuid="cd7a146a-1af0-4428-9b88-dcb159691656"> <reportElement stretchType="RelativeToTallestObject" x="220" y="80" width="60" height="28" uuid="cd7a146a-1af0-4428-9b88-dcb159691656">
<property name="com.jaspersoft.studio.unit.y" value="px"/> <property name="com.jaspersoft.studio.unit.y" value="px"/>
@@ -471,7 +484,7 @@
<textFieldExpression><![CDATA[$F{storeLocation}]]></textFieldExpression> <textFieldExpression><![CDATA[$F{storeLocation}]]></textFieldExpression>
</textField> </textField>
<textField textAdjust="StretchHeight"> <textField textAdjust="StretchHeight">
<reportElement x="690" y="2" width="108" height="18" uuid="eb6ed0fc-bfda-4a89-a163-fe08b00a0120">
<reportElement x="690" y="2" width="65" height="18" uuid="eb6ed0fc-bfda-4a89-a163-fe08b00a0120">
<property name="com.jaspersoft.studio.unit.height" value="px"/> <property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement> </reportElement>
<textElement textAlignment="Left" verticalAlignment="Top"> <textElement textAlignment="Left" verticalAlignment="Top">
@@ -479,6 +492,15 @@
</textElement> </textElement>
<textFieldExpression><![CDATA[$F{supplierName}]]></textFieldExpression> <textFieldExpression><![CDATA[$F{supplierName}]]></textFieldExpression>
</textField> </textField>
<textField textAdjust="StretchHeight">
<reportElement x="758" y="2" width="42" height="18" uuid="f2e3d4c5-b6a7-8901-cdef-234567890abc">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="微軟正黑體" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$F{poM18CreatorDisplay}]]></textFieldExpression>
</textField>
</band> </band>
</detail> </detail>
</jasperReport> </jasperReport>

Yükleniyor…
İptal
Kaydet