| @@ -3,6 +3,7 @@ package com.ffii.core.utils | |||
| import org.apache.pdfbox.pdmodel.PDDocument | |||
| import org.apache.pdfbox.rendering.PDFRenderer | |||
| import org.apache.pdfbox.rendering.ImageType | |||
| import java.awt.Graphics2D | |||
| import java.awt.image.BufferedImage | |||
| import java.io.File | |||
| import java.io.OutputStream | |||
| @@ -15,18 +16,27 @@ import java.util.* | |||
| */ | |||
| 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. | |||
| * | |||
| * @param pdfFile The PDF file to be printed. | |||
| * @param printerIp The IP address of the Zebra printer. | |||
| * @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. | |||
| */ | |||
| @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 | |||
| if (!pdfFile.exists() || !pdfFile.canRead()) { | |||
| @@ -43,14 +53,16 @@ open class ZebraPrinterUtil { | |||
| val image = renderer.renderImage(0, 300 / 72f, ImageType.BINARY) | |||
| // 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 | |||
| 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) { | |||
| @@ -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. | |||
| * This function handles the conversion of pixel data to a compressed hex string. | |||
| * | |||
| * @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. | |||
| */ | |||
| 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() | |||
| 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 | |||
| val bytesPerRow = (width + 7) / 8 | |||
| val totalBytes = bytesPerRow * height | |||
| 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 | |||
| for (y in 0 until height) { | |||
| val rowBits = BitSet(width) | |||
| for (x in 0 until width) { | |||
| // 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) | |||
| } | |||
| } | |||
| @@ -5,7 +5,7 @@ import com.ffii.fpsms.m18.M18Config | |||
| import com.ffii.fpsms.m18.service.* | |||
| import com.ffii.fpsms.m18.web.models.M18CommonRequest | |||
| 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.Items | |||
| 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.fpsms.m18.service.M18DeliveryOrderService | |||
| @@ -14,7 +14,6 @@ import org.slf4j.LoggerFactory | |||
| import org.springframework.scheduling.TaskScheduler | |||
| import org.springframework.scheduling.support.CronTrigger | |||
| import org.springframework.stereotype.Service | |||
| import java.time.LocalDate | |||
| import java.time.LocalDateTime | |||
| import java.time.format.DateTimeFormatter | |||
| 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 | |||
| import com.ffii.core.support.AbstractRepository | |||
| import com.ffii.fpsms.modules.master.entity.projections.WarehouseCombo | |||
| import org.springframework.stereotype.Repository | |||
| @Repository | |||
| 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.Warehouse | |||
| 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.stock.entity.InventoryLotRepository | |||
| import com.ffii.fpsms.modules.stock.entity.StockInLine | |||
| @@ -23,4 +24,8 @@ open class WarehouseService( | |||
| open fun getWarehouses(): List<Warehouse> { | |||
| 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.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.projections.DetailedProdScheduleWithLine | |||
| 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.validation.Valid | |||
| import org.springframework.web.bind.annotation.* | |||
| import java.time.Duration | |||
| import java.time.LocalDate | |||
| import java.time.LocalDateTime | |||
| import java.time.format.DateTimeFormatter | |||
| import java.util.HashMap | |||
| import kotlin.collections.component1 | |||
| import kotlin.collections.component2 | |||
| import kotlin.math.abs | |||
| @RestController | |||
| @@ -1,6 +1,7 @@ | |||
| package com.ffii.fpsms.modules.master.web | |||
| 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 org.springframework.web.bind.annotation.GetMapping | |||
| import org.springframework.web.bind.annotation.RequestMapping | |||
| @@ -15,4 +16,9 @@ class WarehouseController( | |||
| fun getWarehouses(): List<Warehouse> { | |||
| return warehouseService.getWarehouses() | |||
| } | |||
| @GetMapping("/combo") | |||
| fun findCombo(): List<WarehouseCombo> { | |||
| return warehouseService.findCombo() | |||
| } | |||
| } | |||
| @@ -28,6 +28,9 @@ interface EscalationLogInfo { | |||
| @get:Value("#{target.stockInLine?.dnDate}") | |||
| val dnDate: LocalDateTime? | |||
| @get:Value("#{target.stockInLine?.item?.code} - #{target.stockInLine?.item?.name}") | |||
| val item: String? | |||
| @get:Value("#{target.stockInLine?.item?.code}") | |||
| val itemCode: String? | |||
| @@ -1,6 +1,8 @@ | |||
| package com.ffii.fpsms.modules.stock.entity | |||
| 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.fpsms.modules.master.entity.Items | |||
| import jakarta.persistence.* | |||
| @@ -11,6 +13,7 @@ import org.hibernate.type.SqlTypes | |||
| import java.time.LocalDate | |||
| import java.time.LocalDateTime | |||
| @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id") | |||
| @Entity | |||
| @Table(name = "inventory_lot") | |||
| open class InventoryLot : BaseEntity<Long>() { | |||
| @@ -38,6 +41,9 @@ open class InventoryLot : BaseEntity<Long>() { | |||
| @Column(name = "lotNo") | |||
| open var lotNo: String? = null | |||
| @OneToMany(mappedBy = "inventoryLot", cascade = [CascadeType.ALL], orphanRemoval = true) | |||
| open var inventoryLotLines: MutableList<InventoryLotLine>? = null | |||
| // @JdbcTypeCode(SqlTypes.JSON) | |||
| // @Column(name = "qrCodeJson") | |||
| // open var qrCodeJson: MutableMap<String, Any>? = null | |||
| @@ -31,4 +31,6 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long | |||
| fun findCurrentInventoryByItems(items: List<Serializable>): List<CurrentInventoryItemInfo> | |||
| fun findAllByIdIn(ids: List<Serializable>): List<InventoryLotLine> | |||
| fun findAllByInventoryLotId(id: Serializable): List<InventoryLotLine> | |||
| } | |||
| @@ -28,13 +28,13 @@ interface InventoryInfo{ | |||
| val availableQty: BigDecimal? | |||
| @get:Value("#{target.item.itemUoms.^[stockUnit == true && deleted == false]?.uom.code}") | |||
| 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? | |||
| // @get:Value("#{target.qty * target.uom.gramPerSmallestUnit}") | |||
| // val germPerSmallestUnit: BigDecimal? | |||
| @get:Value("#{(target.onHandQty - target.onHoldQty - target.unavailableQty)}") | |||
| 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? | |||
| // @get:Value("#{target.qty * (target.uom.unit4 != '' ? target.uom.unit4Qty " + | |||
| // ": target.uom.unit3 != '' ? target.uom.unit3Qty " + | |||
| @@ -1,9 +1,7 @@ | |||
| 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.stock.enums.EscalationLogStatus | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryLot | |||
| import org.springframework.beans.factory.annotation.Value | |||
| import java.math.BigDecimal | |||
| import java.time.LocalDate | |||
| @@ -48,4 +46,22 @@ interface StockInLineInfo { | |||
| val dnDate: LocalDateTime? | |||
| @get:Value("#{target.escalationLog.^[status.value == 'pending']?.handler?.id}") | |||
| 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.LocalDateTime | |||
| 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.WarehouseRepository | |||
| import com.ffii.fpsms.modules.master.service.PrinterService | |||
| import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository | |||
| import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderLineStatus | |||
| import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderStatus | |||
| import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | |||
| 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.enums.EscalationLogStatus | |||
| import java.io.FileNotFoundException | |||
| import java.time.format.DateTimeFormatter | |||
| import kotlinx.serialization.Serializable | |||
| import kotlinx.serialization.json.Json | |||
| import kotlinx.serialization.encodeToString | |||
| import net.sf.jasperreports.engine.JasperExportManager | |||
| import net.sf.jasperreports.engine.JasperPrint | |||
| import java.io.File | |||
| import kotlin.math.max | |||
| @Serializable | |||
| @@ -58,6 +63,7 @@ open class StockInLineService( | |||
| private val itemRepository: ItemsRepository, | |||
| private val warehouseRepository: WarehouseRepository, | |||
| private val itemUomRespository: ItemUomRespository, | |||
| private val printerService: PrinterService, | |||
| ): AbstractBaseEntityService<StockInLine, Long, StockInLineRepository>(jdbcDao, stockInLineRepository) { | |||
| open fun getStockInLineInfo(stockInLineId: Long): StockInLineInfo { | |||
| @@ -139,26 +145,53 @@ open class StockInLineService( | |||
| @Throws(IOException::class) | |||
| @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) | |||
| @@ -221,7 +254,7 @@ open class StockInLineService( | |||
| @Throws(IOException::class) | |||
| @Transactional | |||
| fun updatePurchaseOrderStatus(request: SaveStockInLineRequest) { | |||
| open fun updatePurchaseOrderStatus(request: SaveStockInLineRequest) { | |||
| if (request.status == StockInLineStatus.COMPLETE.status) { | |||
| val unfinishedLines = polRepository | |||
| .findAllByPurchaseOrderIdAndStatusNotAndDeletedIsFalse(purchaseOrderId = request.purchaseOrderId, status = PurchaseOrderLineStatus.COMPLETED) | |||
| @@ -320,10 +353,23 @@ open class StockInLineService( | |||
| // Putaway | |||
| 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) { | |||
| // QC | |||
| @@ -481,7 +527,7 @@ open class StockInLineService( | |||
| field["uom"] = info.uom.code.toString() | |||
| field["productionDate"] = info.productionDate?.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!! | |||
| val image = QrCodeUtil.generateQRCodeImage(qrCodeContent) | |||
| field["qrCode"] = image | |||
| @@ -495,4 +541,36 @@ open class StockInLineService( | |||
| "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.service.StockInLineService | |||
| 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 jakarta.servlet.http.HttpServletResponse | |||
| import jakarta.validation.Valid | |||
| import net.sf.jasperreports.engine.JasperExportManager | |||
| import net.sf.jasperreports.engine.JasperPrint | |||
| import org.springframework.context.NoSuchMessageException | |||
| import org.springframework.http.ResponseEntity | |||
| import org.springframework.web.bind.annotation.* | |||
| import java.io.OutputStream | |||
| import java.io.UnsupportedEncodingException | |||
| @@ -53,5 +55,8 @@ class StockInLineController( | |||
| 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 escalationLog: SaveEscalationLogRequest?, | |||
| 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`) | |||
| ); | |||