| @@ -1,6 +0,0 @@ | |||||
| package com.ffii.fpsms.enums | |||||
| enum class Status(val value: Int) { | |||||
| PENDING(1), | |||||
| COMPLETE(2) | |||||
| } | |||||
| @@ -16,7 +16,7 @@ open class M18TokenService( | |||||
| private val m18Config: M18Config | private val m18Config: M18Config | ||||
| ) { | ) { | ||||
| @PostConstruct | |||||
| // @Bean | |||||
| fun run() { | fun run() { | ||||
| // val params: MutableMap<String, String> = mutableMapOf( | // val params: MutableMap<String, String> = mutableMapOf( | ||||
| // "grant_type" to m18Config.GRANT_TYPE, | // "grant_type" to m18Config.GRANT_TYPE, | ||||
| @@ -0,0 +1,35 @@ | |||||
| package com.ffii.fpsms.modules.stock.entity | |||||
| import com.ffii.core.entity.BaseEntity | |||||
| import jakarta.persistence.Column | |||||
| import jakarta.persistence.Entity | |||||
| import jakarta.persistence.JoinColumn | |||||
| import jakarta.persistence.OneToOne | |||||
| import jakarta.persistence.Table | |||||
| import jakarta.validation.constraints.NotNull | |||||
| @Entity | |||||
| @Table(name = "stock_ledger") | |||||
| open class StockLedger: BaseEntity<Long>() { | |||||
| // @OneToOne | |||||
| // @JoinColumn(name = "stockInLineId") | |||||
| // open var stockInLine: StockInLine? = null | |||||
| @OneToOne | |||||
| @JoinColumn(name = "stockOutLineId") | |||||
| open var stockOutLine: StockOutLine? = null | |||||
| @NotNull | |||||
| @OneToOne | |||||
| @JoinColumn(name = "inventoryId") | |||||
| open var inventory: Inventory? = null | |||||
| @Column(name = "inQty") | |||||
| open var inQty: Double? = null | |||||
| @Column(name = "outQty") | |||||
| open var outQty: Double? = null | |||||
| @Column(name = "balance") | |||||
| open var balance: Double? = null | |||||
| } | |||||
| @@ -0,0 +1,8 @@ | |||||
| package com.ffii.fpsms.modules.stock.entity | |||||
| import com.ffii.core.support.AbstractRepository | |||||
| import org.springframework.stereotype.Repository | |||||
| @Repository | |||||
| interface StockLedgerRepository: AbstractRepository<StockLedger, Long> { | |||||
| } | |||||
| @@ -0,0 +1,41 @@ | |||||
| package com.ffii.fpsms.modules.stock.entity | |||||
| import com.ffii.core.entity.BaseEntity | |||||
| import jakarta.persistence.Column | |||||
| import jakarta.persistence.Entity | |||||
| import jakarta.persistence.Table | |||||
| import jakarta.validation.constraints.NotNull | |||||
| import java.time.LocalDateTime | |||||
| @Entity | |||||
| @Table(name = "stock_out") | |||||
| open class StockOut : BaseEntity<Long>(){ | |||||
| @NotNull | |||||
| @Column(name = "type") | |||||
| open var type: String? = null | |||||
| @Column(name = "deliveryOrderCode") | |||||
| open var deliveryOrderCode: String? = null | |||||
| @Column(name = "pickOrderCode") | |||||
| open var pickOrderCode: String? = null | |||||
| @Column(name = "consoCode") | |||||
| open var consoCode: String? = null | |||||
| @Column(name = "completeDate") | |||||
| open var completeDate: LocalDateTime? = null | |||||
| @Column(name = "status") | |||||
| open var status: String? = null | |||||
| @Column(name = "handlerId") | |||||
| open var handler: Long? = null | |||||
| @Column(name = "targetOutletId") | |||||
| open var targetOutletId: Long? = null | |||||
| @Column(name = "remarks") | |||||
| open var remarks: String? = null | |||||
| } | |||||
| @@ -0,0 +1,11 @@ | |||||
| package com.ffii.fpsms.modules.stock.entity | |||||
| import com.ffii.core.support.AbstractRepository | |||||
| import com.ffii.fpsms.modules.stock.web.model.StockOutStatus | |||||
| import org.springframework.stereotype.Repository | |||||
| @Repository | |||||
| interface StockOutLIneRepository: AbstractRepository<StockOutLine, Long> { | |||||
| fun findAllByStockOutIdAndDeletedFalse(stockOutId: Long): List<StockOutLine> | |||||
| // fun findAllByStockOutIdAndDeletedFalse(stockOutId: Long, status: StockOutStatus): List<StockOutLine> | |||||
| } | |||||
| @@ -0,0 +1,39 @@ | |||||
| package com.ffii.fpsms.modules.stock.entity | |||||
| import com.ffii.core.entity.BaseEntity | |||||
| import com.ffii.fpsms.modules.master.entity.Items | |||||
| import jakarta.persistence.* | |||||
| import jakarta.validation.constraints.NotNull | |||||
| import java.time.LocalDateTime | |||||
| @Entity | |||||
| @Table(name = "stock_out_line") | |||||
| open class StockOutLine: BaseEntity<Long>() { | |||||
| @NotNull | |||||
| @OneToOne | |||||
| @JoinColumn(name = "itemId") | |||||
| open var item: Items? = null | |||||
| @NotNull | |||||
| @Column(name = "qty") | |||||
| open var qty: Double? = null | |||||
| @NotNull | |||||
| @JoinColumn(name = "stockOutId") | |||||
| open var stockOut: StockOut? = null | |||||
| @NotNull | |||||
| @OneToOne | |||||
| @JoinColumn(name = "inventoryId") | |||||
| open var inventory: Inventory? = null | |||||
| @NotNull | |||||
| @Column(name = "status") | |||||
| open var status: String? = null | |||||
| @Column(name = "pickTime") | |||||
| open var pickTime: LocalDateTime? = null | |||||
| @Column(name = "pickerId") | |||||
| open var pickerId: Long? = null | |||||
| } | |||||
| @@ -0,0 +1,8 @@ | |||||
| package com.ffii.fpsms.modules.stock.entity | |||||
| import com.ffii.core.support.AbstractRepository | |||||
| import org.springframework.stereotype.Repository | |||||
| @Repository | |||||
| interface StockOutRepository: AbstractRepository<StockOut, Long> { | |||||
| } | |||||
| @@ -8,6 +8,7 @@ import com.ffii.fpsms.modules.master.entity.ItemsRepository | |||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | import com.ffii.fpsms.modules.master.web.models.MessageResponse | ||||
| import com.ffii.fpsms.modules.stock.entity.Inventory | import com.ffii.fpsms.modules.stock.entity.Inventory | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryRepository | import com.ffii.fpsms.modules.stock.entity.InventoryRepository | ||||
| import com.ffii.fpsms.modules.stock.service.InventoryService.SQL.INVENTORY_COUNT | |||||
| import com.ffii.fpsms.modules.stock.web.model.SaveInventoryRequest | import com.ffii.fpsms.modules.stock.web.model.SaveInventoryRequest | ||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import java.io.IOException | import java.io.IOException | ||||
| @@ -25,22 +26,28 @@ open class InventoryService( | |||||
| val inventory = inventoryRepository.findAll() | val inventory = inventoryRepository.findAll() | ||||
| return inventory | return inventory | ||||
| } | } | ||||
| object SQL { | |||||
| val INVENTORY_COUNT = StringBuilder("select" | |||||
| + " count(id) " | |||||
| + " from inventory i " | |||||
| + " where i.created >= :from " | |||||
| + " and i.created = :to " | |||||
| + " and i.itemId = :itemId" | |||||
| ) | |||||
| } | |||||
| @Throws(IOException::class) | @Throws(IOException::class) | ||||
| open fun updateInventory(request: SaveInventoryRequest): MessageResponse { | open fun updateInventory(request: SaveInventoryRequest): MessageResponse { | ||||
| // out need id | // out need id | ||||
| // in not necessary | // in not necessary | ||||
| val reqQty = if (request.type === "out") { | |||||
| request.qty * -1 | |||||
| } else { | |||||
| request.qty | |||||
| } | |||||
| var reqQty = request.qty | |||||
| if (request.type === "out") reqQty *= -1 | |||||
| if (request.id !== null) { // old record | if (request.id !== null) { // old record | ||||
| val inventory = inventoryRepository.findById(request.id).orElseThrow() | val inventory = inventoryRepository.findById(request.id).orElseThrow() | ||||
| val newStatus = request.status ?: inventory.status | val newStatus = request.status ?: inventory.status | ||||
| val newExpiry = request.expiryDate ?: inventory.expiryDate | val newExpiry = request.expiryDate ?: inventory.expiryDate | ||||
| // uom should be changing | |||||
| // stock in line should be changing | |||||
| // item id should be changing | |||||
| // uom should not be changing | |||||
| // stock in line should not be changing | |||||
| // item id should not be changing | |||||
| inventory.apply { | inventory.apply { | ||||
| qty = inventory.qty!! + reqQty | qty = inventory.qty!! + reqQty | ||||
| expiryDate = newExpiry | expiryDate = newExpiry | ||||
| @@ -64,15 +71,10 @@ open class InventoryService( | |||||
| val args = mapOf( | val args = mapOf( | ||||
| "from" to from, | "from" to from, | ||||
| "to" to to, | "to" to to, | ||||
| ) | |||||
| val sql = StringBuilder("select" | |||||
| + " count(id) " | |||||
| + " from inventory i " | |||||
| + " where i.created >= :from " | |||||
| + " and i.created = :to " | |||||
| "itemId" to item.id | |||||
| ) | ) | ||||
| val prefix = "LOT" | val prefix = "LOT" | ||||
| val count = jdbcDao.queryForInt(sql.toString(), args) | |||||
| val count = jdbcDao.queryForInt(INVENTORY_COUNT.toString(), args) | |||||
| val newLotNo = CodeGenerator.generateCode(prefix, item.id!!, count) | val newLotNo = CodeGenerator.generateCode(prefix, item.id!!, count) | ||||
| val newExpiry = request.expiryDate | val newExpiry = request.expiryDate | ||||
| inventory.apply { | inventory.apply { | ||||
| @@ -0,0 +1,119 @@ | |||||
| package com.ffii.fpsms.modules.stock.service | |||||
| import com.ffii.core.support.AbstractBaseEntityService | |||||
| import com.ffii.core.support.JdbcDao | |||||
| import com.ffii.fpsms.modules.master.entity.ItemsRepository | |||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||||
| import com.ffii.fpsms.modules.stock.entity.* | |||||
| import com.ffii.fpsms.modules.stock.web.model.SaveStockOutLineRequest | |||||
| import com.ffii.fpsms.modules.stock.web.model.SaveStockOutRequest | |||||
| import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus | |||||
| import com.ffii.fpsms.modules.stock.web.model.StockOutStatus | |||||
| import org.springframework.stereotype.Service | |||||
| import org.springframework.transaction.annotation.Transactional | |||||
| import java.io.IOException | |||||
| import java.time.LocalDate | |||||
| import java.time.LocalDateTime | |||||
| @Service | |||||
| open class StockOutLineService( | |||||
| private val jdbcDao: JdbcDao, | |||||
| private val stockOutRepository: StockOutRepository, | |||||
| private val stockOutLIneRepository: StockOutLIneRepository, | |||||
| private val itemRepository: ItemsRepository, | |||||
| private val inventoryRepository: InventoryRepository, | |||||
| ): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLIneRepository) { | |||||
| @Throws(IOException::class) | |||||
| @Transactional | |||||
| open fun findAllByStockOutId(stockOutId: Long): List<StockOutLine> { | |||||
| return stockOutLIneRepository.findAllByStockOutIdAndDeletedFalse(stockOutId) | |||||
| } | |||||
| @Throws(IOException::class) | |||||
| @Transactional | |||||
| open fun deleteById(id: Long): String { | |||||
| val deleteStockOutline = stockOutLIneRepository.findById(id).orElseThrow().apply { | |||||
| deleted = true | |||||
| } | |||||
| val updateItem = stockOutLIneRepository.save(deleteStockOutline) | |||||
| val response = "mark deleted item: ${updateItem.id}" | |||||
| return response | |||||
| } | |||||
| @Throws(IOException::class) | |||||
| @Transactional | |||||
| open fun deleteByStockOutId(id: Long): String { | |||||
| val deleteStockOutline = stockOutLIneRepository.findAllByStockOutIdAndDeletedFalse(id).map {sol -> | |||||
| sol.apply { | |||||
| deleted = true | |||||
| } | |||||
| } | |||||
| val updateItemIds = stockOutLIneRepository.saveAllAndFlush(deleteStockOutline).map { it.id } | |||||
| val response = "mark deleted items: $updateItemIds" | |||||
| return response | |||||
| } | |||||
| @Throws(IOException::class) | |||||
| @Transactional | |||||
| /// only create?????? | |||||
| open fun create(request: SaveStockOutLineRequest): MessageResponse { | |||||
| val stockOutLine = StockOutLine() | |||||
| val item = itemRepository.findById(request.itemId).orElseThrow() | |||||
| stockOutLine.apply { | |||||
| this.item = item | |||||
| qty = request.qty | |||||
| status = StockOutLineStatus.PENDING.status //create the base record = the original total sum of one item | |||||
| } | |||||
| val savedStockOutLine = stockOutLIneRepository.saveAndFlush(stockOutLine) | |||||
| return MessageResponse( | |||||
| id = savedStockOutLine.id, | |||||
| code = item.code, | |||||
| name = item.name, | |||||
| type = savedStockOutLine.status, | |||||
| message = "save success", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| // stock out -> stock out line record : {qty = 100} | |||||
| // each pick create new stock out line {...sol, status = picked} | |||||
| // when confirm all picked and complete in web | |||||
| // update: | |||||
| // 1.stock out status | |||||
| // remove: | |||||
| // 1.stock out line : {qty = 100} | |||||
| @Throws(IOException::class) | |||||
| @Transactional | |||||
| open fun pick(request: SaveStockOutLineRequest): MessageResponse { | |||||
| val item = itemRepository.findById(request.itemId).orElseThrow() | |||||
| if (request.inventoryId === null) { | |||||
| return MessageResponse( | |||||
| id = request.id, | |||||
| code = null, | |||||
| name = item.name, | |||||
| type = null, | |||||
| message = "inventory is null or not exist", | |||||
| errorPosition = "inventoryId" | |||||
| ) | |||||
| } | |||||
| val stockOutLine = StockOutLine() | |||||
| val _stockOut = stockOutRepository.findById(request.stockOutId).orElseThrow() | |||||
| val _inventory = inventoryRepository.findById(request.inventoryId).orElseThrow() | |||||
| val picker = request.pickerId | |||||
| stockOutLine.apply { | |||||
| this.item = item | |||||
| qty = request.qty | |||||
| stockOut = _stockOut | |||||
| inventory = _inventory | |||||
| status = "picked" | |||||
| pickTime = LocalDateTime.now() | |||||
| pickerId = picker | |||||
| } | |||||
| val savedOutLine = stockOutLIneRepository.saveAndFlush(stockOutLine) | |||||
| return MessageResponse( | |||||
| id = savedOutLine.id, | |||||
| code = null, | |||||
| name = null, | |||||
| type = savedOutLine.status, | |||||
| message = "item picked", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,125 @@ | |||||
| package com.ffii.fpsms.modules.stock.service | |||||
| import com.ffii.core.support.AbstractBaseEntityService | |||||
| import com.ffii.core.support.JdbcDao | |||||
| import com.ffii.fpsms.modules.master.entity.Items | |||||
| import com.ffii.fpsms.modules.master.entity.ItemsRepository | |||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||||
| import com.ffii.fpsms.modules.stock.entity.* | |||||
| import com.ffii.fpsms.modules.stock.web.model.SaveStockOutRequest | |||||
| import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus | |||||
| import com.ffii.fpsms.modules.stock.web.model.StockOutStatus | |||||
| import org.springframework.stereotype.Service | |||||
| import org.springframework.transaction.annotation.Transactional | |||||
| import java.io.IOException | |||||
| import java.time.LocalDate | |||||
| import java.time.LocalDateTime | |||||
| @Service | |||||
| open class StockOutService( | |||||
| private val jdbcDao: JdbcDao, | |||||
| private val stockOutRepository: StockOutRepository, | |||||
| private val stockOutLIneRepository: StockOutLIneRepository, | |||||
| private val stockOutLineService: StockOutLineService, | |||||
| private val stockLedgerRepository: StockLedgerRepository, | |||||
| ): AbstractBaseEntityService<StockOut, Long, StockOutRepository>(jdbcDao, stockOutRepository) { | |||||
| @Throws(IOException::class) | |||||
| @Transactional | |||||
| // update record data | |||||
| open fun saveStockOut(request: SaveStockOutRequest): MessageResponse { | |||||
| val stockOut = if (request.id !== null) stockOutRepository.findById(request.id).orElseThrow() else StockOut() | |||||
| stockOut.apply { | |||||
| type = request.type | |||||
| deliveryOrderCode = request.deliveryOrderCode | |||||
| pickOrderCode = request.pickOrderCode | |||||
| consoCode = request.consoCode | |||||
| completeDate = request.completeDate | |||||
| handler = request.handler | |||||
| targetOutletId = request.targetOutletId | |||||
| remarks = request.remarks | |||||
| } | |||||
| val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | |||||
| // only create stock out line, no update | |||||
| for (outLine in request.stockOutLine) { | |||||
| stockOutLineService.create(outLine) | |||||
| } | |||||
| return MessageResponse( | |||||
| id = savedStockOut.id, | |||||
| code = savedStockOut.pickOrderCode ?: savedStockOut.deliveryOrderCode , | |||||
| name = savedStockOut.consoCode, | |||||
| type = savedStockOut.type, | |||||
| message = "save success", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| @Throws(IOException::class) | |||||
| @Transactional | |||||
| open fun complete(request: SaveStockOutRequest): MessageResponse { | |||||
| if (request.id === null) { | |||||
| return MessageResponse( | |||||
| id = null, | |||||
| code = null, | |||||
| name = null, | |||||
| type = null, | |||||
| message = "id cannot be null", | |||||
| errorPosition = "id" | |||||
| ) | |||||
| } | |||||
| /// checking pick quantity | |||||
| val stockOut = stockOutRepository.findById(request.id).orElseThrow() | |||||
| val allLines = stockOutLineService.findAllByStockOutId(request.id) | |||||
| val isBalanced = allLines.all{ | |||||
| val status = it.status | |||||
| var sum = 0.0 | |||||
| when (status) { | |||||
| StockOutLineStatus.PENDING.status -> sum += it.qty ?: 0.0; | |||||
| StockOutLineStatus.PICKED.status -> sum -= it.qty ?: 0.0; | |||||
| } | |||||
| sum == 0.0 | |||||
| } | |||||
| if (!isBalanced) { | |||||
| return MessageResponse( | |||||
| id = request.id, | |||||
| code = stockOut.consoCode, | |||||
| name = stockOut.pickOrderCode ?: stockOut.deliveryOrderCode, | |||||
| type = stockOut.type, | |||||
| message = "there are items not picked", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| // remove base record | |||||
| val baseLines = allLines.filter { stockOutLine -> stockOutLine.status === StockOutLineStatus.PENDING.status} | |||||
| // update pick record to complete | |||||
| val pickLines = allLines | |||||
| .filter { stockOutLine -> stockOutLine.status === StockOutLineStatus.PICKED.status} | |||||
| .map { stockOutLine -> | |||||
| stockOutLine.apply { | |||||
| status = StockOutLineStatus.COMPLETE.status | |||||
| } | |||||
| } | |||||
| // write ledger | |||||
| val ledgers = pickLines.map {stockOutLine -> | |||||
| StockLedger().apply { | |||||
| this.stockOutLine = stockOutLine | |||||
| this.inventory = stockOutLine.inventory | |||||
| outQty = stockOutLine.qty | |||||
| } | |||||
| } | |||||
| stockLedgerRepository.saveAll(ledgers) | |||||
| stockOutLIneRepository.deleteAll(baseLines) | |||||
| stockOutLIneRepository.saveAll(pickLines) | |||||
| stockOut.apply { | |||||
| completeDate = LocalDateTime.now() | |||||
| status = StockOutStatus.COMPLETE.status | |||||
| } | |||||
| val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | |||||
| return MessageResponse( | |||||
| id = savedStockOut.id, | |||||
| code = savedStockOut.consoCode, | |||||
| name = savedStockOut.pickOrderCode ?: savedStockOut.deliveryOrderCode, | |||||
| type = savedStockOut.type, | |||||
| message = "stock out completed", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,40 @@ | |||||
| package com.ffii.fpsms.modules.stock.web.model | |||||
| import java.time.LocalDate | |||||
| import java.time.LocalDateTime | |||||
| enum class StockOutStatus(val status: String) { | |||||
| PENDING("pending"), | |||||
| COMPLETE("completed"), | |||||
| // CANCELLED("cancelled") | |||||
| } | |||||
| enum class StockOutLineStatus(val status: String) { | |||||
| PENDING("pending"), | |||||
| PICKED("picked"), | |||||
| COMPLETE("completed"), | |||||
| // CANCELLED("cancelled") | |||||
| } | |||||
| data class SaveStockOutRequest( | |||||
| val id: Long?, | |||||
| val type: String, // delivery || pick || etc | |||||
| val deliveryOrderCode: String?, | |||||
| val pickOrderCode: String?, | |||||
| val consoCode: String?, | |||||
| val completeDate: LocalDateTime?, | |||||
| val status: StockOutStatus?, | |||||
| val handler: Long?, | |||||
| val targetOutletId: Long?, | |||||
| val remarks: String?, | |||||
| val stockOutLine: List<SaveStockOutLineRequest> | |||||
| ) | |||||
| data class SaveStockOutLineRequest( | |||||
| val id: Long?, | |||||
| val itemId: Long, | |||||
| val qty: Double, | |||||
| val stockOutId: Long, | |||||
| val inventoryId: Long?, | |||||
| val status: StockOutLineStatus?, | |||||
| val pickTime: LocalDateTime?, | |||||
| val pickerId: Long? | |||||
| ) | |||||
| @@ -0,0 +1,6 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset derek:m18_raw_data | |||||
| ALTER TABLE `stock_out_line` | |||||
| ADD COLUMN `stockOutId` INT NOT NULL AFTER `qty`, | |||||
| ADD CONSTRAINT FK_STOCKOUT_LINE_STOCKOUT_ID FOREIGN KEY (stockOutId) REFERENCES stock_out (id); | |||||