| @@ -0,0 +1,43 @@ | |||||
| package com.ffii.fpsms.modules.stock.entity | |||||
| import com.fasterxml.jackson.annotation.JsonBackReference | |||||
| import com.ffii.core.entity.BaseEntity | |||||
| import com.ffii.fpsms.modules.master.entity.Items | |||||
| import jakarta.persistence.* | |||||
| import jakarta.validation.constraints.NotNull | |||||
| import java.math.BigDecimal | |||||
| @Entity | |||||
| @Table(name = "stock_adjustment_record") | |||||
| open class StockAdjustmentRecord : BaseEntity<Long>() { | |||||
| @NotNull | |||||
| @ManyToOne | |||||
| @JoinColumn(name = "itemId", nullable = false) | |||||
| open var item: Items? = null | |||||
| @Column(name = "itemCode", length = 50) | |||||
| open var itemCode: String? = null | |||||
| @Column(name = "itemName", length = 200) | |||||
| open var itemName: String? = null | |||||
| @Column(name = "lotNo", length = 512) | |||||
| open var lotNo: String? = null | |||||
| @Column(name = "inQty", precision = 14, scale = 2) | |||||
| open var inQty: BigDecimal? = null | |||||
| @Column(name = "outQty", precision = 14, scale = 2) | |||||
| open var outQty: BigDecimal? = null | |||||
| @JsonBackReference | |||||
| @ManyToOne | |||||
| @JoinColumn(name = "stockInLineId") | |||||
| open var stockInLine: StockInLine? = null | |||||
| @JsonBackReference | |||||
| @ManyToOne | |||||
| @JoinColumn(name = "stockOutLineId") | |||||
| open var stockOutLine: StockOutLine? = null | |||||
| } | |||||
| @@ -0,0 +1,7 @@ | |||||
| package com.ffii.fpsms.modules.stock.entity | |||||
| import com.ffii.core.support.AbstractRepository | |||||
| import org.springframework.stereotype.Repository | |||||
| @Repository | |||||
| interface StockAdjustmentRecordRepository : AbstractRepository<StockAdjustmentRecord, Long> | |||||
| @@ -40,6 +40,13 @@ import com.ffii.fpsms.modules.stock.web.model.ScannedLotInfo | |||||
| import com.ffii.fpsms.modules.stock.web.model.SameItemLotInfo | import com.ffii.fpsms.modules.stock.web.model.SameItemLotInfo | ||||
| import com.ffii.fpsms.modules.jobOrder.service.JobOrderService | import com.ffii.fpsms.modules.jobOrder.service.JobOrderService | ||||
| import com.ffii.fpsms.modules.jobOrder.web.model.ExportFGStockInLabelRequest | import com.ffii.fpsms.modules.jobOrder.web.model.ExportFGStockInLabelRequest | ||||
| import com.ffii.fpsms.modules.master.service.PrinterService | |||||
| import com.ffii.fpsms.modules.stock.web.model.PrintLabelForInventoryLotLineRequest | |||||
| import com.ffii.fpsms.modules.jobOrder.web.model.PrintFGStockInLabelRequest | |||||
| import com.ffii.core.utils.ZebraPrinterUtil | |||||
| import net.sf.jasperreports.engine.JasperExportManager | |||||
| import java.io.File | |||||
| import net.sf.jasperreports.engine.JasperPrint | |||||
| @Service | @Service | ||||
| open class InventoryLotLineService( | open class InventoryLotLineService( | ||||
| @@ -49,6 +56,7 @@ open class InventoryLotLineService( | |||||
| private val itemUomRespository: ItemUomRespository, | private val itemUomRespository: ItemUomRespository, | ||||
| private val stockInLineRepository: StockInLineRepository, | private val stockInLineRepository: StockInLineRepository, | ||||
| private val inventoryRepository: InventoryRepository, | private val inventoryRepository: InventoryRepository, | ||||
| private val printerService: PrinterService, | |||||
| @Lazy | @Lazy | ||||
| private val jobOrderService: JobOrderService | private val jobOrderService: JobOrderService | ||||
| ) { | ) { | ||||
| @@ -217,7 +225,6 @@ open class InventoryLotLineService( | |||||
| // .minus(inventoryLotLine.outQty ?: zero) | // .minus(inventoryLotLine.outQty ?: zero) | ||||
| // .minus(inventoryLotLine.holdQty ?: zero) | // .minus(inventoryLotLine.holdQty ?: zero) | ||||
| // Accepted qty at stock-in, no conversion (stays e.g. 50000 even after stock out) | |||||
| field["acceptedQty"] = "%.2f".format(info.acceptedQty) | field["acceptedQty"] = "%.2f".format(info.acceptedQty) | ||||
| val stockItemUom = itemUomRespository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse(info.itemId) | val stockItemUom = itemUomRespository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse(info.itemId) | ||||
| @@ -267,6 +274,56 @@ open class InventoryLotLineService( | |||||
| } | } | ||||
| } | } | ||||
| @Transactional | |||||
| open fun printLabelForInventoryLotLine(request: PrintLabelForInventoryLotLineRequest) { | |||||
| val printer = printerService.findById(request.printerId) | |||||
| ?: throw NoSuchElementException("No such printer") | |||||
| val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() | |||||
| val stockInLine = inventoryLotLine.inventoryLot?.stockInLine | |||||
| when { | |||||
| stockInLine?.jobOrder != null -> { | |||||
| jobOrderService.printFGStockInLabel( | |||||
| PrintFGStockInLabelRequest( | |||||
| stockInLineId = stockInLine.id!!, | |||||
| printerId = request.printerId, | |||||
| printQty = request.printQty | |||||
| ) | |||||
| ) | |||||
| } | |||||
| else -> { | |||||
| val pdf = if (stockInLine?.stockTransferRecord != null) { | |||||
| val targetLocation = stockInLine.stockTransferRecord?.targetLocation ?: "" | |||||
| exportStockInLineQrcode( | |||||
| LotLineToQrcode(inventoryLotLineId = request.inventoryLotLineId, isTransfer = "轉倉至 $targetLocation") | |||||
| ) | |||||
| } else { | |||||
| exportStockInLineQrcode(LotLineToQrcode(inventoryLotLineId = request.inventoryLotLineId)) | |||||
| } | |||||
| val jasperPrint = pdf["report"] as JasperPrint | |||||
| val tempPdfFile = File.createTempFile("print_job_", ".pdf") | |||||
| try { | |||||
| JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath) | |||||
| 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, | |||||
| printer.dpi | |||||
| ) | |||||
| } | |||||
| } | |||||
| } finally { | |||||
| tempPdfFile.delete() | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @Transactional | @Transactional | ||||
| open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantitiesRequest): MessageResponse { | open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantitiesRequest): MessageResponse { | ||||
| try { | try { | ||||
| @@ -0,0 +1,164 @@ | |||||
| package com.ffii.fpsms.modules.stock.service | |||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | |||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository | |||||
| import com.ffii.fpsms.modules.stock.web.model.StockAdjustmentRequest | |||||
| import com.ffii.fpsms.modules.stock.web.model.StockInRequest | |||||
| import com.ffii.fpsms.modules.stock.web.model.StockOutRequest | |||||
| import org.springframework.stereotype.Service | |||||
| import org.springframework.transaction.annotation.Transactional | |||||
| import java.math.BigDecimal | |||||
| import java.time.LocalDate | |||||
| import com.ffii.fpsms.modules.stock.entity.StockAdjustmentRecord | |||||
| import com.ffii.fpsms.modules.stock.entity.StockAdjustmentRecordRepository | |||||
| import com.ffii.fpsms.modules.stock.entity.StockInLine | |||||
| import com.ffii.fpsms.modules.stock.entity.StockOutLine | |||||
| @Service | |||||
| open class StockAdjustmentService( | |||||
| private val stockInLineService: StockInLineService, | |||||
| private val stockOutLineService: StockOutLineService, | |||||
| private val inventoryLotLineRepository: InventoryLotLineRepository, | |||||
| private val stockAdjustmentRecordRepository: StockAdjustmentRecordRepository | |||||
| ) { | |||||
| @Transactional | |||||
| open fun submit(request: StockAdjustmentRequest): MessageResponse { | |||||
| val originalById = request.originalLines.filter { it.id > 0 }.associateBy { it.id } | |||||
| val currentIds = request.currentLines.map { it.id }.toSet() | |||||
| // Branch 4: Removed entries — stock out full qty and mark unavailable | |||||
| val removed = request.originalLines.filter { it.id > 0 && it.id !in currentIds } | |||||
| for (line in removed) { | |||||
| val stockOutLine = stockOutLineService.createStockOut( | |||||
| StockOutRequest( | |||||
| inventoryLotLineId = line.id, | |||||
| qty = line.adjustedQty.toDouble(), | |||||
| type = "ADJ" | |||||
| ) | |||||
| ) | |||||
| saveAdjustmentRecordForStockOut(stockOutLine) | |||||
| } | |||||
| for (current in request.currentLines) { | |||||
| // Branch 2: New entry — createStockIn (OPEN or ADJ) | |||||
| if (current.isNew) { | |||||
| val stockType = if (current.isOpeningInventory) "OPEN" else "ADJ" | |||||
| val stockInLine = stockInLineService.createStockIn( | |||||
| StockInRequest( | |||||
| itemId = current.itemId, | |||||
| itemNo = current.itemNo, | |||||
| demandQty = null, | |||||
| acceptedQty = current.adjustedQty, | |||||
| expiryDate = LocalDate.parse(current.expiryDate), | |||||
| lotNo = current.lotNo?.takeIf { it.isNotBlank() }, | |||||
| productLotNo = current.productlotNo?.takeIf { it.isNotBlank() }, | |||||
| dnNo = current.dnNo?.takeIf { it.isNotBlank() }, | |||||
| type = stockType, | |||||
| warehouseId = current.warehouseId | |||||
| ) | |||||
| ) | |||||
| saveAdjustmentRecordForStockIn(stockInLine) | |||||
| continue | |||||
| } | |||||
| // Branch 1 & 3: Existing line — compare qty | |||||
| val original = originalById[current.id] ?: continue | |||||
| val diff = current.adjustedQty.subtract(original.adjustedQty) | |||||
| if (diff.compareTo(BigDecimal.ZERO) == 0) continue // Branch 1: no change | |||||
| if (diff.compareTo(BigDecimal.ZERO) > 0) { | |||||
| // Branch 2 (qty up): createStockIn | |||||
| val inventoryLotLine = inventoryLotLineRepository.findById(current.id) | |||||
| .orElseThrow { IllegalArgumentException("InventoryLotLine not found: ${current.id}") } | |||||
| val stockInRequest = buildStockInRequestFromExistingLotLine(inventoryLotLine, diff) | |||||
| val stockInLine = stockInLineService.createStockIn(stockInRequest) | |||||
| saveAdjustmentRecordForStockIn(stockInLine) | |||||
| } else { | |||||
| // Branch 3 (qty down): createStockOut | |||||
| val stockOutLine = stockOutLineService.createStockOut( | |||||
| StockOutRequest( | |||||
| inventoryLotLineId = current.id, | |||||
| qty = diff.abs().toDouble(), | |||||
| type = "ADJ" | |||||
| ) | |||||
| ) | |||||
| saveAdjustmentRecordForStockOut(stockOutLine) | |||||
| } | |||||
| } | |||||
| return MessageResponse( | |||||
| id = null, | |||||
| name = "Stock Adjustment", | |||||
| code = "STOCK_ADJUSTMENT_SUBMITTED", | |||||
| type = "success", | |||||
| message = "Stock adjustment completed successfully", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| private fun buildStockInRequestFromExistingLotLine( | |||||
| inventoryLotLine: InventoryLotLine, | |||||
| acceptedQty: BigDecimal | |||||
| ): StockInRequest { | |||||
| val inventoryLot = inventoryLotLine.inventoryLot | |||||
| ?: throw IllegalArgumentException("InventoryLotLine must have an associated InventoryLot") | |||||
| val item = inventoryLot.item | |||||
| ?: throw IllegalArgumentException("InventoryLot must have an associated item") | |||||
| val itemId = item.id | |||||
| ?: throw IllegalArgumentException("Item must have an id") | |||||
| val itemCode = item.code | |||||
| ?: throw IllegalArgumentException("Item must have a code") | |||||
| val expiryDate = inventoryLot.expiryDate | |||||
| ?: throw IllegalArgumentException("InventoryLot must have an expiryDate") | |||||
| val lotNo = inventoryLot.lotNo | |||||
| val productLotNo = inventoryLot.stockInLine?.productLotNo | |||||
| val dnNo = inventoryLot.stockInLine?.dnNo | |||||
| val warehouseId = inventoryLotLine.warehouse?.id | |||||
| ?: throw IllegalArgumentException("InventoryLotLine must have a warehouse") | |||||
| return StockInRequest( | |||||
| itemId = itemId, | |||||
| itemNo = itemCode, | |||||
| demandQty = null, | |||||
| acceptedQty = acceptedQty, | |||||
| expiryDate = expiryDate, | |||||
| lotNo = lotNo, | |||||
| productLotNo = productLotNo, | |||||
| dnNo = dnNo, | |||||
| type = "ADJ", | |||||
| warehouseId = warehouseId | |||||
| ) | |||||
| } | |||||
| private fun saveAdjustmentRecordForStockIn(stockInLine: StockInLine) { | |||||
| val item = stockInLine.item ?: return | |||||
| val record = StockAdjustmentRecord().apply { | |||||
| this.item = item | |||||
| this.itemCode = stockInLine.itemNo ?: item.code | |||||
| this.itemName = item.name | |||||
| this.lotNo = stockInLine.lotNo | |||||
| this.inQty = stockInLine.acceptedQty | |||||
| this.outQty = null | |||||
| this.stockInLine = stockInLine | |||||
| this.stockOutLine = null | |||||
| } | |||||
| stockAdjustmentRecordRepository.save(record) | |||||
| } | |||||
| private fun saveAdjustmentRecordForStockOut(stockOutLine: StockOutLine) { | |||||
| val item = stockOutLine.item ?: return | |||||
| val lotNo = stockOutLine.inventoryLotLine?.inventoryLot?.lotNo | |||||
| val record = StockAdjustmentRecord().apply { | |||||
| this.item = item | |||||
| this.itemCode = item.code | |||||
| this.itemName = item.name | |||||
| this.lotNo = lotNo | |||||
| this.inQty = null | |||||
| this.outQty = BigDecimal.valueOf(stockOutLine.qty ?: 0.0) | |||||
| this.stockInLine = null | |||||
| this.stockOutLine = stockOutLine | |||||
| } | |||||
| stockAdjustmentRecordRepository.save(record) | |||||
| } | |||||
| } | |||||
| @@ -23,6 +23,7 @@ import java.io.UnsupportedEncodingException | |||||
| import java.math.BigDecimal | import java.math.BigDecimal | ||||
| import java.text.ParseException | import java.text.ParseException | ||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | import com.ffii.fpsms.modules.master.web.models.MessageResponse | ||||
| import com.ffii.fpsms.modules.stock.web.model.PrintLabelForInventoryLotLineRequest | |||||
| import com.ffii.fpsms.modules.stock.web.model.UpdateInventoryLotLineStatusRequest | import com.ffii.fpsms.modules.stock.web.model.UpdateInventoryLotLineStatusRequest | ||||
| import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisRequest | import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisRequest | ||||
| import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisResponse | import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisResponse | ||||
| @@ -73,7 +74,7 @@ class InventoryLotLineController ( | |||||
| ) | ) | ||||
| } | } | ||||
| @PostMapping("/print-label") | |||||
| @PostMapping("/download-label") | |||||
| @Throws(UnsupportedEncodingException::class, NoSuchMessageException::class, ParseException::class, Exception::class) | @Throws(UnsupportedEncodingException::class, NoSuchMessageException::class, ParseException::class, Exception::class) | ||||
| fun printLabel(@Valid @RequestBody request: LotLineToQrcode, response: HttpServletResponse) { | fun printLabel(@Valid @RequestBody request: LotLineToQrcode, response: HttpServletResponse) { | ||||
| response.characterEncoding = "utf-8"; | response.characterEncoding = "utf-8"; | ||||
| @@ -84,6 +85,12 @@ class InventoryLotLineController ( | |||||
| response.addHeader("filename", "${pdf["fileName"]}.pdf") | response.addHeader("filename", "${pdf["fileName"]}.pdf") | ||||
| out.write(JasperExportManager.exportReportToPdf(jasperPrint)); | out.write(JasperExportManager.exportReportToPdf(jasperPrint)); | ||||
| } | } | ||||
| @GetMapping("/print-label") | |||||
| fun printLabel(@ModelAttribute request: PrintLabelForInventoryLotLineRequest) { | |||||
| inventoryLotLineService.printLabelForInventoryLotLine(request) | |||||
| } | |||||
| @PostMapping("/updateStatus") | @PostMapping("/updateStatus") | ||||
| fun updateInventoryLotLineStatus(@RequestBody request: UpdateInventoryLotLineStatusRequest): MessageResponse { | fun updateInventoryLotLineStatus(@RequestBody request: UpdateInventoryLotLineStatusRequest): MessageResponse { | ||||
| println("=== DEBUG: updateInventoryLotLineStatus Controller ===") | println("=== DEBUG: updateInventoryLotLineStatus Controller ===") | ||||
| @@ -0,0 +1,18 @@ | |||||
| package com.ffii.fpsms.modules.stock.web | |||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||||
| import com.ffii.fpsms.modules.stock.service.StockAdjustmentService | |||||
| import com.ffii.fpsms.modules.stock.web.model.StockAdjustmentRequest | |||||
| import jakarta.validation.Valid | |||||
| import org.springframework.web.bind.annotation.* | |||||
| @RestController | |||||
| @RequestMapping("/stockAdjustment") | |||||
| class StockAdjustmentController( | |||||
| private val stockAdjustmentService: StockAdjustmentService | |||||
| ) { | |||||
| @PostMapping("/submit") | |||||
| fun submit(@Valid @RequestBody request: StockAdjustmentRequest): MessageResponse { | |||||
| return stockAdjustmentService.submit(request) | |||||
| } | |||||
| } | |||||
| @@ -43,7 +43,7 @@ class StockInLineController( | |||||
| return stockInLineService.update(newItem) | return stockInLineService.update(newItem) | ||||
| } | } | ||||
| @PostMapping("/print-label") | |||||
| @PostMapping("/download-label") | |||||
| @Throws(UnsupportedEncodingException::class, NoSuchMessageException::class, ParseException::class, Exception::class) | @Throws(UnsupportedEncodingException::class, NoSuchMessageException::class, ParseException::class, Exception::class) | ||||
| fun printLabel(@Valid @RequestBody request: ExportQrCodeRequest, response: HttpServletResponse) { | fun printLabel(@Valid @RequestBody request: ExportQrCodeRequest, response: HttpServletResponse) { | ||||
| response.characterEncoding = "utf-8"; | response.characterEncoding = "utf-8"; | ||||
| @@ -55,7 +55,7 @@ class StockInLineController( | |||||
| out.write(JasperExportManager.exportReportToPdf(jasperPrint)); | out.write(JasperExportManager.exportReportToPdf(jasperPrint)); | ||||
| } | } | ||||
| @GetMapping("/printQrCode") | |||||
| @GetMapping("/print-label") | |||||
| fun printQrCode(@ModelAttribute request: PrintQrCodeForSilRequest) { | fun printQrCode(@ModelAttribute request: PrintQrCodeForSilRequest) { | ||||
| stockInLineService.printQrCode(request) | stockInLineService.printQrCode(request) | ||||
| } | } | ||||
| @@ -0,0 +1,7 @@ | |||||
| package com.ffii.fpsms.modules.stock.web.model | |||||
| data class PrintLabelForInventoryLotLineRequest( | |||||
| val inventoryLotLineId: Long, | |||||
| val printerId: Long, | |||||
| val printQty: Int? = null, | |||||
| ) | |||||
| @@ -0,0 +1,24 @@ | |||||
| package com.ffii.fpsms.modules.stock.web.model | |||||
| import java.math.BigDecimal | |||||
| data class StockAdjustmentLineRequest( | |||||
| val id: Long, | |||||
| val lotNo: String? = null, | |||||
| val adjustedQty: BigDecimal, | |||||
| val productlotNo: String? = null, | |||||
| val dnNo: String? = null, | |||||
| val isOpeningInventory: Boolean = false, | |||||
| val isNew: Boolean = false, | |||||
| val itemId: Long, | |||||
| val itemNo: String, | |||||
| val expiryDate: String, | |||||
| val warehouseId: Long, | |||||
| val uom: String? = null | |||||
| ) | |||||
| data class StockAdjustmentRequest( | |||||
| val itemId: Long, | |||||
| val originalLines: List<StockAdjustmentLineRequest>, | |||||
| val currentLines: List<StockAdjustmentLineRequest> | |||||
| ) | |||||
| @@ -0,0 +1,29 @@ | |||||
| -- liquibase formatted sql | |||||
| -- changeset KelvinY:create_stock_adjustment_record_table | |||||
| CREATE TABLE IF NOT EXISTS `stock_adjustment_record` ( | |||||
| `id` BIGINT NOT NULL AUTO_INCREMENT, | |||||
| `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||||
| `createdBy` VARCHAR(255) DEFAULT NULL, | |||||
| `version` INT NOT NULL DEFAULT 0, | |||||
| `modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, | |||||
| `modifiedBy` VARCHAR(255) DEFAULT NULL, | |||||
| `deleted` TINYINT(1) NOT NULL DEFAULT 0, | |||||
| `itemId` INT NOT NULL, | |||||
| `itemCode` VARCHAR(50) DEFAULT NULL, | |||||
| `itemName` VARCHAR(200) DEFAULT NULL, | |||||
| `lotNo` VARCHAR(512) DEFAULT NULL, | |||||
| `inQty` DECIMAL(14,2) DEFAULT NULL, | |||||
| `outQty` DECIMAL(14,2) DEFAULT NULL, | |||||
| `stockInLineId` INT DEFAULT NULL, | |||||
| `stockOutLineId` INT DEFAULT NULL, | |||||
| PRIMARY KEY (`id`), | |||||
| INDEX `idx_stock_adjustment_record_itemId` (`itemId`), | |||||
| INDEX `idx_stock_adjustment_record_stockInLineId` (`stockInLineId`), | |||||
| INDEX `idx_stock_adjustment_record_stockOutLineId` (`stockOutLineId`), | |||||
| CONSTRAINT `fk_stock_adjustment_record_item` FOREIGN KEY (`itemId`) REFERENCES `items` (`id`), | |||||
| CONSTRAINT `fk_stock_adjustment_record_stock_in_line` FOREIGN KEY (`stockInLineId`) REFERENCES `stock_in_line` (`id`), | |||||
| CONSTRAINT `fk_stock_adjustment_record_stock_out_line` FOREIGN KEY (`stockOutLineId`) REFERENCES `stock_out_line` (`id`) | |||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||||