| @@ -3,6 +3,7 @@ package com.ffii.core.utils | |||||
| import org.apache.pdfbox.pdmodel.PDDocument | import org.apache.pdfbox.pdmodel.PDDocument | ||||
| import org.apache.pdfbox.rendering.PDFRenderer | import org.apache.pdfbox.rendering.PDFRenderer | ||||
| import org.apache.pdfbox.rendering.ImageType | import org.apache.pdfbox.rendering.ImageType | ||||
| import java.awt.Graphics2D | |||||
| import java.awt.image.BufferedImage | import java.awt.image.BufferedImage | ||||
| import java.io.File | import java.io.File | ||||
| import java.io.OutputStream | import java.io.OutputStream | ||||
| @@ -15,18 +16,27 @@ import java.util.* | |||||
| */ | */ | ||||
| open class ZebraPrinterUtil { | open class ZebraPrinterUtil { | ||||
| companion object { | |||||
| // Enum to represent valid ZPL print directions | |||||
| enum class PrintDirection(val zplCode: String, val degree: Double) { | |||||
| NORMAL("N", 0.0), // Normal (0 degrees) | |||||
| ROTATED("R", 90.0), // Rotated 90 degrees clockwise | |||||
| INVERTED("I", 180.0), // Inverted 180 degrees | |||||
| BOTTOM_UP("B", 270.0) // Bottom-up, 270 degrees clockwise | |||||
| } | |||||
| companion object { | |||||
| /** | /** | ||||
| * Converts the first page of a PDF document into a ZPL command and sends it to the specified printer. | * Converts the first page of a PDF document into a ZPL command and sends it to the specified printer. | ||||
| * | * | ||||
| * @param pdfFile The PDF file to be printed. | * @param pdfFile The PDF file to be printed. | ||||
| * @param printerIp The IP address of the Zebra printer. | * @param printerIp The IP address of the Zebra printer. | ||||
| * @param printerPort The port of the Zebra printer, typically 9100. | * @param printerPort The port of the Zebra printer, typically 9100. | ||||
| * @param printQty The qty of print, default 1 | |||||
| * @param printDirection Valid values: N (Normal), R (Rotated 90), I (Inverted 180), B (Bottom-up 270) | |||||
| * @throws Exception if there is an error during file processing or printing. | * @throws Exception if there is an error during file processing or printing. | ||||
| */ | */ | ||||
| @JvmStatic | @JvmStatic | ||||
| fun printPdfToZebra(pdfFile: File, printerIp: String, printerPort: Int) { | |||||
| fun printPdfToZebra(pdfFile: File, printerIp: String, printerPort: Int, printQty: Int? = 1, printDirection: PrintDirection = PrintDirection.NORMAL) { | |||||
| // Check if the file exists and is readable | // Check if the file exists and is readable | ||||
| if (!pdfFile.exists() || !pdfFile.canRead()) { | if (!pdfFile.exists() || !pdfFile.canRead()) { | ||||
| @@ -43,14 +53,16 @@ open class ZebraPrinterUtil { | |||||
| val image = renderer.renderImage(0, 300 / 72f, ImageType.BINARY) | val image = renderer.renderImage(0, 300 / 72f, ImageType.BINARY) | ||||
| // 3. Convert the image to a ZPL format string | // 3. Convert the image to a ZPL format string | ||||
| val zplCommand = convertImageToZpl(image) | |||||
| val zplCommand = convertImageToZpl(image, printDirection) | |||||
| // 4. Send the ZPL command to the printer via a network socket | // 4. Send the ZPL command to the printer via a network socket | ||||
| val printData = zplCommand.toByteArray() | val printData = zplCommand.toByteArray() | ||||
| Socket(printerIp, printerPort).use { socket -> | |||||
| val os: OutputStream = socket.getOutputStream() | |||||
| os.write(printData) | |||||
| os.flush() | |||||
| repeat(printQty ?: 1) { index -> | |||||
| Socket(printerIp, printerPort).use { socket -> | |||||
| val os: OutputStream = socket.getOutputStream() | |||||
| os.write(printData) | |||||
| os.flush() | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } catch (e: Exception) { | } catch (e: Exception) { | ||||
| @@ -59,32 +71,60 @@ open class ZebraPrinterUtil { | |||||
| } | } | ||||
| } | } | ||||
| /** | |||||
| * Rotates a BufferedImage by the specified angle. | |||||
| * | |||||
| * @param image The BufferedImage to rotate. | |||||
| * @param angleDegrees The rotation angle in degrees (clockwise). | |||||
| * @return The rotated BufferedImage. | |||||
| */ | |||||
| private fun rotateImage(image: BufferedImage, angleDegrees: Double): BufferedImage { | |||||
| val rads = Math.toRadians(angleDegrees) | |||||
| val sin = Math.abs(Math.sin(rads)) | |||||
| val cos = Math.abs(Math.cos(rads)) | |||||
| val w = image.width | |||||
| val h = image.height | |||||
| val newWidth = (w * cos + h * sin).toInt() | |||||
| val newHeight = (w * sin + h * cos).toInt() | |||||
| val rotated = BufferedImage(newWidth, newHeight, BufferedImage.TYPE_BYTE_BINARY) | |||||
| val g2d: Graphics2D = rotated.createGraphics() | |||||
| g2d.translate((newWidth - w) / 2, (newHeight - h) / 2) | |||||
| g2d.rotate(rads, w / 2.0, h / 2.0) | |||||
| g2d.drawImage(image, 0, 0, null) | |||||
| g2d.dispose() | |||||
| return rotated | |||||
| } | |||||
| /** | /** | ||||
| * Converts a BufferedImage (monochrome) to a ZPL string using the ^GFA command. | * Converts a BufferedImage (monochrome) to a ZPL string using the ^GFA command. | ||||
| * This function handles the conversion of pixel data to a compressed hex string. | * This function handles the conversion of pixel data to a compressed hex string. | ||||
| * | * | ||||
| * @param image The BufferedImage to convert. | * @param image The BufferedImage to convert. | ||||
| * @param printDirection Valid values: N (Normal), R (Rotated 90), I (Inverted 180), B (Bottom-up 270) | |||||
| * @return A ZPL-formatted string ready to be sent to the printer. | * @return A ZPL-formatted string ready to be sent to the printer. | ||||
| */ | */ | ||||
| private fun convertImageToZpl(image: BufferedImage): String { | |||||
| private fun convertImageToZpl(image: BufferedImage, printDirection: PrintDirection = PrintDirection.NORMAL): String { | |||||
| val rotatedImage = rotateImage(image, printDirection.degree) | |||||
| val zpl = StringBuilder() | val zpl = StringBuilder() | ||||
| zpl.append("^XA\n") | |||||
| zpl.append("^XA") | |||||
| val width = image.width | |||||
| val height = image.height | |||||
| val width = rotatedImage.width | |||||
| val height = rotatedImage.height | |||||
| // ZPL format for a graphical image is ^GFA | // ZPL format for a graphical image is ^GFA | ||||
| val bytesPerRow = (width + 7) / 8 | val bytesPerRow = (width + 7) / 8 | ||||
| val totalBytes = bytesPerRow * height | val totalBytes = bytesPerRow * height | ||||
| zpl.append("^FO0,0^GFA,").append(totalBytes).append(",").append(totalBytes).append(",").append(bytesPerRow).append(",") | zpl.append("^FO0,0^GFA,").append(totalBytes).append(",").append(totalBytes).append(",").append(bytesPerRow).append(",") | ||||
| println(zpl) | |||||
| // Extract pixel data and convert to ZPL hex string | // Extract pixel data and convert to ZPL hex string | ||||
| for (y in 0 until height) { | for (y in 0 until height) { | ||||
| val rowBits = BitSet(width) | val rowBits = BitSet(width) | ||||
| for (x in 0 until width) { | for (x in 0 until width) { | ||||
| // Check for a black pixel (0xFF000000 in ARGB) | // Check for a black pixel (0xFF000000 in ARGB) | ||||
| if (image.getRGB(x, y) == 0xFF000000.toInt()) { | |||||
| if (rotatedImage.getRGB(x, y) == 0xFF000000.toInt()) { | |||||
| rowBits.set(x) | rowBits.set(x) | ||||
| } | } | ||||
| } | } | ||||
| @@ -5,7 +5,7 @@ import com.ffii.fpsms.m18.M18Config | |||||
| import com.ffii.fpsms.m18.service.* | import com.ffii.fpsms.m18.service.* | ||||
| import com.ffii.fpsms.m18.web.models.M18CommonRequest | import com.ffii.fpsms.m18.web.models.M18CommonRequest | ||||
| import com.ffii.fpsms.modules.common.SettingNames | import com.ffii.fpsms.modules.common.SettingNames | ||||
| import com.ffii.fpsms.modules.common.scheduler.SchedulerService | |||||
| import com.ffii.fpsms.modules.common.scheduler.service.SchedulerService | |||||
| import com.ffii.fpsms.modules.master.entity.ItemUom | import com.ffii.fpsms.modules.master.entity.ItemUom | ||||
| import com.ffii.fpsms.modules.master.entity.Items | import com.ffii.fpsms.modules.master.entity.Items | ||||
| import com.ffii.fpsms.modules.master.entity.ShopRepository | import com.ffii.fpsms.modules.master.entity.ShopRepository | ||||
| @@ -1,4 +1,4 @@ | |||||
| package com.ffii.fpsms.modules.common.scheduler | |||||
| package com.ffii.fpsms.modules.common.scheduler.service | |||||
| import com.ffii.core.utils.JwtTokenUtil | import com.ffii.core.utils.JwtTokenUtil | ||||
| import com.ffii.fpsms.m18.service.M18DeliveryOrderService | import com.ffii.fpsms.m18.service.M18DeliveryOrderService | ||||
| @@ -14,7 +14,6 @@ import org.slf4j.LoggerFactory | |||||
| import org.springframework.scheduling.TaskScheduler | import org.springframework.scheduling.TaskScheduler | ||||
| import org.springframework.scheduling.support.CronTrigger | import org.springframework.scheduling.support.CronTrigger | ||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import java.time.LocalDate | |||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||
| import java.util.HashMap | import java.util.HashMap | ||||
| @@ -0,0 +1,32 @@ | |||||
| package com.ffii.fpsms.modules.master.entity | |||||
| import com.ffii.core.entity.BaseEntity | |||||
| import jakarta.persistence.Column | |||||
| import jakarta.persistence.Entity | |||||
| import jakarta.persistence.Id | |||||
| import jakarta.persistence.Table | |||||
| import jakarta.validation.constraints.Size | |||||
| @Entity | |||||
| @Table(name = "printer") | |||||
| open class Printer : BaseEntity<Long>() { | |||||
| @Size(max = 500) | |||||
| @Column(name = "code", length = 500) | |||||
| open var code: String? = null | |||||
| @Size(max = 500) | |||||
| @Column(name = "name", length = 500) | |||||
| open var name: String? = null | |||||
| @Size(max = 500) | |||||
| @Column(name = "description", length = 500) | |||||
| open var description: String? = null | |||||
| @Size(max = 30) | |||||
| @Column(name = "ip", length = 30) | |||||
| open var ip: String? = null | |||||
| @Size(max = 10) | |||||
| @Column(name = "port", length = 10) | |||||
| open var port: Int? = null | |||||
| } | |||||
| @@ -0,0 +1,13 @@ | |||||
| package com.ffii.fpsms.modules.master.entity | |||||
| import com.ffii.core.support.AbstractRepository | |||||
| import com.ffii.fpsms.modules.master.entity.projections.PrinterCombo | |||||
| import org.springframework.stereotype.Repository | |||||
| import java.io.Serializable | |||||
| @Repository | |||||
| interface PrinterRepository : AbstractRepository<Printer, Long> { | |||||
| fun findPrinterComboByDeletedFalse(): List<PrinterCombo>; | |||||
| fun findByIdAndDeletedFalse(id: Serializable): Printer?; | |||||
| } | |||||
| @@ -1,8 +1,10 @@ | |||||
| package com.ffii.fpsms.modules.master.entity | package com.ffii.fpsms.modules.master.entity | ||||
| import com.ffii.core.support.AbstractRepository | import com.ffii.core.support.AbstractRepository | ||||
| import com.ffii.fpsms.modules.master.entity.projections.WarehouseCombo | |||||
| import org.springframework.stereotype.Repository | import org.springframework.stereotype.Repository | ||||
| @Repository | @Repository | ||||
| interface WarehouseRepository : AbstractRepository<Warehouse, Long> { | interface WarehouseRepository : AbstractRepository<Warehouse, Long> { | ||||
| fun findWarehouseComboByDeletedFalse(): List<WarehouseCombo>; | |||||
| } | } | ||||
| @@ -0,0 +1,16 @@ | |||||
| package com.ffii.fpsms.modules.master.entity.projections | |||||
| import org.springframework.beans.factory.annotation.Value | |||||
| interface PrinterCombo { | |||||
| val id: Long; | |||||
| @get:Value("#{target.name}") | |||||
| val label: String?; | |||||
| @get:Value("#{target.id}") | |||||
| val value: String; | |||||
| val code: String?; | |||||
| val name: String?; | |||||
| val description: String?; | |||||
| val ip: String?; | |||||
| val port: Int?; | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| package com.ffii.fpsms.modules.master.entity.projections | |||||
| import org.springframework.beans.factory.annotation.Value | |||||
| interface WarehouseCombo { | |||||
| val id: Long; | |||||
| @get:Value("#{target.id}") | |||||
| val value: Long; | |||||
| @get:Value("#{target.code} - #{target.name}") | |||||
| val label: String; | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| package com.ffii.fpsms.modules.master.service | |||||
| import com.ffii.fpsms.modules.master.entity.Printer | |||||
| import com.ffii.fpsms.modules.master.entity.PrinterRepository | |||||
| import com.ffii.fpsms.modules.master.entity.projections.PrinterCombo | |||||
| import org.springframework.stereotype.Service | |||||
| @Service | |||||
| open class PrinterService( | |||||
| val printerRepository: PrinterRepository | |||||
| ) { | |||||
| open fun findCombo(): List<PrinterCombo> { | |||||
| return printerRepository.findPrinterComboByDeletedFalse(); | |||||
| } | |||||
| open fun findById(id: Long): Printer? { | |||||
| return printerRepository.findByIdAndDeletedFalse(id) | |||||
| } | |||||
| } | |||||
| @@ -5,6 +5,7 @@ import com.ffii.core.support.JdbcDao | |||||
| import com.ffii.fpsms.modules.master.entity.ItemsRepository | import com.ffii.fpsms.modules.master.entity.ItemsRepository | ||||
| import com.ffii.fpsms.modules.master.entity.Warehouse | import com.ffii.fpsms.modules.master.entity.Warehouse | ||||
| import com.ffii.fpsms.modules.master.entity.WarehouseRepository | import com.ffii.fpsms.modules.master.entity.WarehouseRepository | ||||
| import com.ffii.fpsms.modules.master.entity.projections.WarehouseCombo | |||||
| import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLineRepository | import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLineRepository | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository | import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository | ||||
| import com.ffii.fpsms.modules.stock.entity.StockInLine | import com.ffii.fpsms.modules.stock.entity.StockInLine | ||||
| @@ -23,4 +24,8 @@ open class WarehouseService( | |||||
| open fun getWarehouses(): List<Warehouse> { | open fun getWarehouses(): List<Warehouse> { | ||||
| return warehouseRepository.findAll().filter { it.deleted == false } | return warehouseRepository.findAll().filter { it.deleted == false } | ||||
| } | } | ||||
| open fun findCombo(): List<WarehouseCombo> { | |||||
| return warehouseRepository.findWarehouseComboByDeletedFalse(); | |||||
| } | |||||
| } | } | ||||
| @@ -0,0 +1,18 @@ | |||||
| package com.ffii.fpsms.modules.master.web | |||||
| import com.ffii.fpsms.modules.master.entity.projections.PrinterCombo | |||||
| import com.ffii.fpsms.modules.master.service.PrinterService | |||||
| import org.springframework.web.bind.annotation.GetMapping | |||||
| import org.springframework.web.bind.annotation.RequestMapping | |||||
| import org.springframework.web.bind.annotation.RestController | |||||
| @RequestMapping("printers") | |||||
| @RestController | |||||
| class PrinterController( | |||||
| private val printerService: PrinterService | |||||
| ) { | |||||
| @GetMapping("/combo") | |||||
| fun findCombo(): List<PrinterCombo> { | |||||
| return printerService.findCombo(); | |||||
| } | |||||
| } | |||||
| @@ -2,7 +2,7 @@ package com.ffii.fpsms.modules.master.web | |||||
| import com.ffii.core.response.RecordsRes | import com.ffii.core.response.RecordsRes | ||||
| import com.ffii.core.utils.CriteriaArgsBuilder | import com.ffii.core.utils.CriteriaArgsBuilder | ||||
| import com.ffii.fpsms.modules.common.scheduler.SchedulerService | |||||
| import com.ffii.fpsms.modules.common.scheduler.service.SchedulerService | |||||
| import com.ffii.fpsms.modules.master.entity.ProductionScheduleRepository | import com.ffii.fpsms.modules.master.entity.ProductionScheduleRepository | ||||
| import com.ffii.fpsms.modules.master.entity.projections.DetailedProdScheduleWithLine | import com.ffii.fpsms.modules.master.entity.projections.DetailedProdScheduleWithLine | ||||
| import com.ffii.fpsms.modules.master.entity.projections.ProdScheduleInfo | import com.ffii.fpsms.modules.master.entity.projections.ProdScheduleInfo | ||||
| @@ -16,14 +16,12 @@ import com.ffii.fpsms.modules.master.web.models.SearchProdScheduleRequest | |||||
| import jakarta.servlet.http.HttpServletRequest | import jakarta.servlet.http.HttpServletRequest | ||||
| import jakarta.validation.Valid | import jakarta.validation.Valid | ||||
| import org.springframework.web.bind.annotation.* | import org.springframework.web.bind.annotation.* | ||||
| import java.time.Duration | |||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||
| import java.util.HashMap | import java.util.HashMap | ||||
| import kotlin.collections.component1 | import kotlin.collections.component1 | ||||
| import kotlin.collections.component2 | import kotlin.collections.component2 | ||||
| import kotlin.math.abs | |||||
| @RestController | @RestController | ||||
| @@ -1,6 +1,7 @@ | |||||
| package com.ffii.fpsms.modules.master.web | package com.ffii.fpsms.modules.master.web | ||||
| import com.ffii.fpsms.modules.master.entity.Warehouse | import com.ffii.fpsms.modules.master.entity.Warehouse | ||||
| import com.ffii.fpsms.modules.master.entity.projections.WarehouseCombo | |||||
| import com.ffii.fpsms.modules.master.service.WarehouseService | import com.ffii.fpsms.modules.master.service.WarehouseService | ||||
| import org.springframework.web.bind.annotation.GetMapping | import org.springframework.web.bind.annotation.GetMapping | ||||
| import org.springframework.web.bind.annotation.RequestMapping | import org.springframework.web.bind.annotation.RequestMapping | ||||
| @@ -15,4 +16,9 @@ class WarehouseController( | |||||
| fun getWarehouses(): List<Warehouse> { | fun getWarehouses(): List<Warehouse> { | ||||
| return warehouseService.getWarehouses() | return warehouseService.getWarehouses() | ||||
| } | } | ||||
| @GetMapping("/combo") | |||||
| fun findCombo(): List<WarehouseCombo> { | |||||
| return warehouseService.findCombo() | |||||
| } | |||||
| } | } | ||||
| @@ -28,6 +28,9 @@ interface EscalationLogInfo { | |||||
| @get:Value("#{target.stockInLine?.dnDate}") | @get:Value("#{target.stockInLine?.dnDate}") | ||||
| val dnDate: LocalDateTime? | val dnDate: LocalDateTime? | ||||
| @get:Value("#{target.stockInLine?.item?.code} - #{target.stockInLine?.item?.name}") | |||||
| val item: String? | |||||
| @get:Value("#{target.stockInLine?.item?.code}") | @get:Value("#{target.stockInLine?.item?.code}") | ||||
| val itemCode: String? | val itemCode: String? | ||||
| @@ -1,6 +1,8 @@ | |||||
| package com.ffii.fpsms.modules.stock.entity | package com.ffii.fpsms.modules.stock.entity | ||||
| import com.fasterxml.jackson.annotation.JsonBackReference | import com.fasterxml.jackson.annotation.JsonBackReference | ||||
| import com.fasterxml.jackson.annotation.JsonIdentityInfo | |||||
| import com.fasterxml.jackson.annotation.ObjectIdGenerators | |||||
| import com.ffii.core.entity.BaseEntity | import com.ffii.core.entity.BaseEntity | ||||
| import com.ffii.fpsms.modules.master.entity.Items | import com.ffii.fpsms.modules.master.entity.Items | ||||
| import jakarta.persistence.* | import jakarta.persistence.* | ||||
| @@ -11,6 +13,7 @@ import org.hibernate.type.SqlTypes | |||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||
| @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id") | |||||
| @Entity | @Entity | ||||
| @Table(name = "inventory_lot") | @Table(name = "inventory_lot") | ||||
| open class InventoryLot : BaseEntity<Long>() { | open class InventoryLot : BaseEntity<Long>() { | ||||
| @@ -38,6 +41,9 @@ open class InventoryLot : BaseEntity<Long>() { | |||||
| @Column(name = "lotNo") | @Column(name = "lotNo") | ||||
| open var lotNo: String? = null | open var lotNo: String? = null | ||||
| @OneToMany(mappedBy = "inventoryLot", cascade = [CascadeType.ALL], orphanRemoval = true) | |||||
| open var inventoryLotLines: MutableList<InventoryLotLine>? = null | |||||
| // @JdbcTypeCode(SqlTypes.JSON) | // @JdbcTypeCode(SqlTypes.JSON) | ||||
| // @Column(name = "qrCodeJson") | // @Column(name = "qrCodeJson") | ||||
| // open var qrCodeJson: MutableMap<String, Any>? = null | // open var qrCodeJson: MutableMap<String, Any>? = null | ||||
| @@ -31,4 +31,6 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long | |||||
| fun findCurrentInventoryByItems(items: List<Serializable>): List<CurrentInventoryItemInfo> | fun findCurrentInventoryByItems(items: List<Serializable>): List<CurrentInventoryItemInfo> | ||||
| fun findAllByIdIn(ids: List<Serializable>): List<InventoryLotLine> | fun findAllByIdIn(ids: List<Serializable>): List<InventoryLotLine> | ||||
| fun findAllByInventoryLotId(id: Serializable): List<InventoryLotLine> | |||||
| } | } | ||||
| @@ -28,13 +28,13 @@ interface InventoryInfo{ | |||||
| val availableQty: BigDecimal? | val availableQty: BigDecimal? | ||||
| @get:Value("#{target.item.itemUoms.^[stockUnit == true && deleted == false]?.uom.code}") | @get:Value("#{target.item.itemUoms.^[stockUnit == true && deleted == false]?.uom.code}") | ||||
| val uomCode: String? | val uomCode: String? | ||||
| @get:Value("#{target.item.itemUoms.^[stockUnit == true && deleted == false]?.uom.udfudesc}") | |||||
| @get:Value("#{target.item.itemUoms.^[stockUnit == true && deleted == false]?.uom?.udfudesc}") | |||||
| val uomUdfudesc: String? | val uomUdfudesc: String? | ||||
| // @get:Value("#{target.qty * target.uom.gramPerSmallestUnit}") | // @get:Value("#{target.qty * target.uom.gramPerSmallestUnit}") | ||||
| // val germPerSmallestUnit: BigDecimal? | // val germPerSmallestUnit: BigDecimal? | ||||
| @get:Value("#{(target.onHandQty - target.onHoldQty - target.unavailableQty)}") | @get:Value("#{(target.onHandQty - target.onHoldQty - target.unavailableQty)}") | ||||
| val qtyPerSmallestUnit: BigDecimal? | val qtyPerSmallestUnit: BigDecimal? | ||||
| @get:Value("#{target.item.itemUoms.^[baseUnit == true && deleted == false]?.uom.udfudesc}") | |||||
| @get:Value("#{target.item.itemUoms.^[baseUnit == true && deleted == false]?.uom?.udfudesc}") | |||||
| val baseUom: String? | val baseUom: String? | ||||
| // @get:Value("#{target.qty * (target.uom.unit4 != '' ? target.uom.unit4Qty " + | // @get:Value("#{target.qty * (target.uom.unit4 != '' ? target.uom.unit4Qty " + | ||||
| // ": target.uom.unit3 != '' ? target.uom.unit3Qty " + | // ": target.uom.unit3 != '' ? target.uom.unit3Qty " + | ||||
| @@ -1,9 +1,7 @@ | |||||
| package com.ffii.fpsms.modules.stock.entity.projection | package com.ffii.fpsms.modules.stock.entity.projection | ||||
| import com.ffii.fpsms.modules.master.entity.Items | |||||
| import com.ffii.fpsms.modules.master.entity.ItemsRepository | |||||
| import com.ffii.fpsms.modules.master.entity.UomConversion | import com.ffii.fpsms.modules.master.entity.UomConversion | ||||
| import com.ffii.fpsms.modules.stock.enums.EscalationLogStatus | |||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLot | |||||
| import org.springframework.beans.factory.annotation.Value | import org.springframework.beans.factory.annotation.Value | ||||
| import java.math.BigDecimal | import java.math.BigDecimal | ||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| @@ -48,4 +46,22 @@ interface StockInLineInfo { | |||||
| val dnDate: LocalDateTime? | val dnDate: LocalDateTime? | ||||
| @get:Value("#{target.escalationLog.^[status.value == 'pending']?.handler?.id}") | @get:Value("#{target.escalationLog.^[status.value == 'pending']?.handler?.id}") | ||||
| val handlerId: Long? | val handlerId: Long? | ||||
| @get:Value("#{target.inventoryLot?.inventoryLotLines ?: new java.util.ArrayList()}") | |||||
| val putAwayLines: List<PutAwayLineForSil>; | |||||
| } | |||||
| interface PutAwayLineForSil { | |||||
| val id: Long?; | |||||
| @get:Value("#{target.inQty " + | |||||
| "* ((target.inventoryLot.item.itemUoms.^[stockUnit == true && deleted == false]?.ratioN / target.inventoryLot.item.itemUoms.^[stockUnit == true && deleted == false]?.ratioD))" + | |||||
| "/ ((target.inventoryLot.item.itemUoms.^[purchaseUnit == true && deleted == false]?.ratioN / target.inventoryLot.item.itemUoms.^[purchaseUnit == true && deleted == false]?.ratioD))}") | |||||
| val qty: BigDecimal?; | |||||
| @get:Value("#{target.warehouse?.code} - #{target.warehouse?.name}") | |||||
| val warehouse: String?; | |||||
| @get:Value("#{target.warehouse?.id}") | |||||
| val warehouseId: Long?; | |||||
| @get:Value("#{target.warehouse?.code}") | |||||
| val warehouseCode: String?; | |||||
| @get:Value("#{target.warehouse?.name}") | |||||
| val warehouseName: String?; | |||||
| } | } | ||||
| @@ -22,20 +22,25 @@ import java.math.BigDecimal | |||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||
| import com.ffii.core.utils.PdfUtils; | import com.ffii.core.utils.PdfUtils; | ||||
| import com.ffii.core.utils.ZebraPrinterUtil | |||||
| import com.ffii.fpsms.modules.master.entity.ItemUomRespository | import com.ffii.fpsms.modules.master.entity.ItemUomRespository | ||||
| import com.ffii.fpsms.modules.master.entity.WarehouseRepository | import com.ffii.fpsms.modules.master.entity.WarehouseRepository | ||||
| import com.ffii.fpsms.modules.master.service.PrinterService | |||||
| import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository | import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository | ||||
| import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderLineStatus | import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderLineStatus | ||||
| import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderStatus | import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderStatus | ||||
| import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | ||||
| import com.ffii.fpsms.modules.stock.entity.projection.StockInLineInfo | import com.ffii.fpsms.modules.stock.entity.projection.StockInLineInfo | ||||
| import com.ffii.fpsms.modules.stock.enums.EscalationLogStatus | |||||
| import com.ffii.fpsms.modules.stock.web.model.* | import com.ffii.fpsms.modules.stock.web.model.* | ||||
| import com.ffii.fpsms.modules.stock.enums.EscalationLogStatus | |||||
| import java.io.FileNotFoundException | import java.io.FileNotFoundException | ||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||
| import kotlinx.serialization.Serializable | import kotlinx.serialization.Serializable | ||||
| import kotlinx.serialization.json.Json | import kotlinx.serialization.json.Json | ||||
| import kotlinx.serialization.encodeToString | import kotlinx.serialization.encodeToString | ||||
| import net.sf.jasperreports.engine.JasperExportManager | |||||
| import net.sf.jasperreports.engine.JasperPrint | |||||
| import java.io.File | |||||
| import kotlin.math.max | import kotlin.math.max | ||||
| @Serializable | @Serializable | ||||
| @@ -58,6 +63,7 @@ open class StockInLineService( | |||||
| private val itemRepository: ItemsRepository, | private val itemRepository: ItemsRepository, | ||||
| private val warehouseRepository: WarehouseRepository, | private val warehouseRepository: WarehouseRepository, | ||||
| private val itemUomRespository: ItemUomRespository, | private val itemUomRespository: ItemUomRespository, | ||||
| private val printerService: PrinterService, | |||||
| ): AbstractBaseEntityService<StockInLine, Long, StockInLineRepository>(jdbcDao, stockInLineRepository) { | ): AbstractBaseEntityService<StockInLine, Long, StockInLineRepository>(jdbcDao, stockInLineRepository) { | ||||
| open fun getStockInLineInfo(stockInLineId: Long): StockInLineInfo { | open fun getStockInLineInfo(stockInLineId: Long): StockInLineInfo { | ||||
| @@ -139,26 +145,53 @@ open class StockInLineService( | |||||
| @Throws(IOException::class) | @Throws(IOException::class) | ||||
| @Transactional | @Transactional | ||||
| fun saveInventoryLotLineWhenStockIn(request: SaveStockInLineRequest, stockInLine: StockInLine): InventoryLotLine { | |||||
| val inventoryLotLine = InventoryLotLine() | |||||
| val warehouse = warehouseRepository.findById(request.warehouseId!!).orElseThrow() | |||||
| val stockItemUom = itemUomRespository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse( | |||||
| itemId = request.itemId | |||||
| ) | |||||
| val convertedBaseQty = if (stockItemUom != null) { | |||||
| (request.acceptQty?:request.acceptedQty) * stockItemUom.ratioN!! / stockItemUom.ratioD!! | |||||
| } else { | |||||
| (request.acceptQty?:request.acceptedQty) | |||||
| } | |||||
| inventoryLotLine.apply { | |||||
| this.inventoryLot = stockInLine.inventoryLot | |||||
| this.warehouse = warehouse | |||||
| this.inQty = convertedBaseQty | |||||
| this.status = InventoryLotLineStatus.AVAILABLE | |||||
| this.stockUom = stockItemUom | |||||
| fun saveInventoryLotLineWhenStockIn(request: SaveStockInLineRequest, stockInLine: StockInLine): List<InventoryLotLine> { | |||||
| val response = request.inventoryLotLines?.let { lines -> | |||||
| val saveLines = mutableListOf<InventoryLotLine>(); | |||||
| lines.forEach { line -> | |||||
| val inventoryLotLine = InventoryLotLine() | |||||
| val warehouse = warehouseRepository.findById(line.warehouseId!!).orElseThrow() | |||||
| val stockItemUom = itemUomRespository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse( | |||||
| itemId = request.itemId | |||||
| ) | |||||
| val purchaseItemUom = itemUomRespository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(request.itemId) | |||||
| val convertedBaseQty = if (stockItemUom != null && purchaseItemUom != null) { | |||||
| (line.qty) * (purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!) | |||||
| } else { | |||||
| (line.qty) | |||||
| } | |||||
| inventoryLotLine.apply { | |||||
| this.inventoryLot = stockInLine.inventoryLot | |||||
| this.warehouse = warehouse | |||||
| this.inQty = convertedBaseQty | |||||
| this.status = InventoryLotLineStatus.AVAILABLE | |||||
| this.stockUom = stockItemUom | |||||
| } | |||||
| saveLines.add(inventoryLotLine) | |||||
| } | |||||
| inventoryLotLineRepository.saveAll(saveLines) | |||||
| } | } | ||||
| val savedInventoryLotLine = inventoryLotLineRepository.saveAndFlush(inventoryLotLine) | |||||
| return savedInventoryLotLine | |||||
| return response ?: emptyList(); | |||||
| // val inventoryLotLine = InventoryLotLine() | |||||
| // val warehouse = warehouseRepository.findById(request.warehouseId!!).orElseThrow() | |||||
| // val stockItemUom = itemUomRespository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse( | |||||
| // itemId = request.itemId | |||||
| // ) | |||||
| // val convertedBaseQty = if (stockItemUom != null) { | |||||
| // (request.acceptQty?:request.acceptedQty) * stockItemUom.ratioN!! / stockItemUom.ratioD!! | |||||
| // } else { | |||||
| // (request.acceptQty?:request.acceptedQty) | |||||
| // } | |||||
| // inventoryLotLine.apply { | |||||
| // this.inventoryLot = stockInLine.inventoryLot | |||||
| // this.warehouse = warehouse | |||||
| // this.inQty = convertedBaseQty | |||||
| // this.status = InventoryLotLineStatus.AVAILABLE | |||||
| // this.stockUom = stockItemUom | |||||
| // } | |||||
| // val savedInventoryLotLine = inventoryLotLineRepository.saveAndFlush(inventoryLotLine) | |||||
| // return savedInventoryLotLine | |||||
| } | } | ||||
| @Throws(IOException::class) | @Throws(IOException::class) | ||||
| @@ -221,7 +254,7 @@ open class StockInLineService( | |||||
| @Throws(IOException::class) | @Throws(IOException::class) | ||||
| @Transactional | @Transactional | ||||
| fun updatePurchaseOrderStatus(request: SaveStockInLineRequest) { | |||||
| open fun updatePurchaseOrderStatus(request: SaveStockInLineRequest) { | |||||
| if (request.status == StockInLineStatus.COMPLETE.status) { | if (request.status == StockInLineStatus.COMPLETE.status) { | ||||
| val unfinishedLines = polRepository | val unfinishedLines = polRepository | ||||
| .findAllByPurchaseOrderIdAndStatusNotAndDeletedIsFalse(purchaseOrderId = request.purchaseOrderId, status = PurchaseOrderLineStatus.COMPLETED) | .findAllByPurchaseOrderIdAndStatusNotAndDeletedIsFalse(purchaseOrderId = request.purchaseOrderId, status = PurchaseOrderLineStatus.COMPLETED) | ||||
| @@ -320,10 +353,23 @@ open class StockInLineService( | |||||
| // Putaway | // Putaway | ||||
| var savedInventoryLotLine: InventoryLotLine? = null | var savedInventoryLotLine: InventoryLotLine? = null | ||||
| savedInventoryLotLine = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine) | |||||
| stockInLine.apply { | |||||
| this.status = StockInLineStatus.COMPLETE.status | |||||
| this.inventoryLotLine = savedInventoryLotLine | |||||
| // savedInventoryLotLine = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine) | |||||
| val savedInventoryLotLines = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine) | |||||
| val inventoryLotLines = stockInLine.inventoryLot?.let { it.id?.let { _id -> inventoryLotLineRepository.findAllByInventoryLotId(_id) } } ?: listOf() | |||||
| val purchaseItemUom = itemUomRespository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(request.itemId) | |||||
| val stockItemUom = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(request.itemId) | |||||
| val ratio = if (stockItemUom != null && purchaseItemUom != null) { | |||||
| (purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!) | |||||
| } else { | |||||
| BigDecimal.ONE | |||||
| } | |||||
| if (inventoryLotLines.sumOf { it.inQty ?: BigDecimal.ZERO } >= request.acceptQty?.times(ratio)) { | |||||
| stockInLine.apply { | |||||
| this.status = StockInLineStatus.COMPLETE.status | |||||
| // this.inventoryLotLine = savedInventoryLotLine | |||||
| } | |||||
| } | } | ||||
| } else if (request.status == StockInLineStatus.PENDING.status || request.status == StockInLineStatus.ESCALATED.status) { | } else if (request.status == StockInLineStatus.PENDING.status || request.status == StockInLineStatus.ESCALATED.status) { | ||||
| // QC | // QC | ||||
| @@ -481,7 +527,7 @@ open class StockInLineService( | |||||
| field["uom"] = info.uom.code.toString() | field["uom"] = info.uom.code.toString() | ||||
| field["productionDate"] = info.productionDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | field["productionDate"] = info.productionDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | ||||
| field["expiryDate"] = info.expiryDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | field["expiryDate"] = info.expiryDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | ||||
| field["lotNo"] = info.lotNo!! | |||||
| field["lotNo"] = info.lotNo ?: "" | |||||
| field["supplier"] = info.supplier!! | field["supplier"] = info.supplier!! | ||||
| val image = QrCodeUtil.generateQRCodeImage(qrCodeContent) | val image = QrCodeUtil.generateQRCodeImage(qrCodeContent) | ||||
| field["qrCode"] = image | field["qrCode"] = image | ||||
| @@ -495,4 +541,36 @@ open class StockInLineService( | |||||
| "fileName" to qrCodeInfo[0].poCode | "fileName" to qrCodeInfo[0].poCode | ||||
| ); | ); | ||||
| } | } | ||||
| @Transactional | |||||
| open fun printQrCode(request: PrintQrCodeForSilRequest) { | |||||
| val printer = printerService.findById(request.printerId) ?: throw NoSuchElementException("No such printer"); | |||||
| val pdf = exportStockInLineQrcode( | |||||
| ExportQrCodeRequest( | |||||
| stockInLineIds = listOf(request.stockInLineId) | |||||
| ) | |||||
| ) | |||||
| val jasperPrint = pdf["report"] as JasperPrint | |||||
| // 1. Create a temporary file to save the PDF. | |||||
| val tempPdfFile = File.createTempFile("print_job_", ".pdf") | |||||
| try { | |||||
| // 2. Export the JasperPrint to the temporary PDF file. | |||||
| JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath) | |||||
| // 3. Call the utility function with the temporary file. | |||||
| val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | |||||
| printer.ip?.let { ip -> printer.port?.let { port -> | |||||
| ZebraPrinterUtil.printPdfToZebra(tempPdfFile, ip, port, printQty, | |||||
| ZebraPrinterUtil.PrintDirection.ROTATED | |||||
| ) | |||||
| } } | |||||
| } finally { | |||||
| // 4. Ensure the temporary file is deleted after the print job is sent. | |||||
| tempPdfFile.delete() | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -6,12 +6,14 @@ import com.ffii.fpsms.modules.stock.entity.StockInLine | |||||
| import com.ffii.fpsms.modules.stock.entity.projection.StockInLineInfo | import com.ffii.fpsms.modules.stock.entity.projection.StockInLineInfo | ||||
| import com.ffii.fpsms.modules.stock.service.StockInLineService | import com.ffii.fpsms.modules.stock.service.StockInLineService | ||||
| import com.ffii.fpsms.modules.stock.web.model.ExportQrCodeRequest | import com.ffii.fpsms.modules.stock.web.model.ExportQrCodeRequest | ||||
| import com.ffii.fpsms.modules.stock.web.model.PrintQrCodeForSilRequest | |||||
| import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest | import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest | ||||
| import jakarta.servlet.http.HttpServletResponse | import jakarta.servlet.http.HttpServletResponse | ||||
| import jakarta.validation.Valid | import jakarta.validation.Valid | ||||
| import net.sf.jasperreports.engine.JasperExportManager | import net.sf.jasperreports.engine.JasperExportManager | ||||
| import net.sf.jasperreports.engine.JasperPrint | import net.sf.jasperreports.engine.JasperPrint | ||||
| import org.springframework.context.NoSuchMessageException | import org.springframework.context.NoSuchMessageException | ||||
| import org.springframework.http.ResponseEntity | |||||
| import org.springframework.web.bind.annotation.* | import org.springframework.web.bind.annotation.* | ||||
| import java.io.OutputStream | import java.io.OutputStream | ||||
| import java.io.UnsupportedEncodingException | import java.io.UnsupportedEncodingException | ||||
| @@ -53,5 +55,8 @@ class StockInLineController( | |||||
| out.write(JasperExportManager.exportReportToPdf(jasperPrint)); | out.write(JasperExportManager.exportReportToPdf(jasperPrint)); | ||||
| } | } | ||||
| @GetMapping("/printQrCode") | |||||
| fun printQrCode(@ModelAttribute request: PrintQrCodeForSilRequest) { | |||||
| stockInLineService.printQrCode(request) | |||||
| } | |||||
| } | } | ||||
| @@ -0,0 +1,8 @@ | |||||
| package com.ffii.fpsms.modules.stock.web.model | |||||
| // Stock in line | |||||
| data class PrintQrCodeForSilRequest( | |||||
| val stockInLineId: Long, | |||||
| val printerId: Long, | |||||
| val printQty: Int?, | |||||
| ) | |||||
| @@ -56,5 +56,11 @@ data class SaveStockInLineRequest( | |||||
| var qcResult: List<SaveQcResultRequest>?, | var qcResult: List<SaveQcResultRequest>?, | ||||
| var escalationLog: SaveEscalationLogRequest?, | var escalationLog: SaveEscalationLogRequest?, | ||||
| var warehouseId: Long?, | var warehouseId: Long?, | ||||
| var rejectQty: BigDecimal? | |||||
| var rejectQty: BigDecimal?, | |||||
| var inventoryLotLines: List<SaveInventoryLotLineForSil>? | |||||
| ) | |||||
| data class SaveInventoryLotLineForSil ( | |||||
| val qty: BigDecimal, | |||||
| val warehouseId: Long? | |||||
| ) | ) | ||||
| @@ -0,0 +1,19 @@ | |||||
| -- liquibase formatted sql | |||||
| -- changeset cyril:create_printer | |||||
| CREATE TABLE `printer` | |||||
| ( | |||||
| `id` INT NOT NULL AUTO_INCREMENT, | |||||
| `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||||
| `createdBy` VARCHAR(30) NULL DEFAULT NULL, | |||||
| `version` INT NOT NULL DEFAULT '0', | |||||
| `modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||||
| `modifiedBy` VARCHAR(30) NULL DEFAULT NULL, | |||||
| `deleted` TINYINT(1) NOT NULL DEFAULT '0', | |||||
| `code` VARCHAR(500) NULL, | |||||
| `name` VARCHAR(500) NULL, | |||||
| `description` VARCHAR(500) NULL, | |||||
| `ip` VARCHAR(30) NULL, | |||||
| `port` INT(10) NULL, | |||||
| PRIMARY KEY (`id`) | |||||
| ); | |||||