| @@ -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 | |||
| ) { | |||
| @PostConstruct | |||
| // @Bean | |||
| fun run() { | |||
| // val params: MutableMap<String, String> = mutableMapOf( | |||
| // "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.stock.entity.Inventory | |||
| 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 org.springframework.stereotype.Service | |||
| import java.io.IOException | |||
| @@ -25,22 +26,28 @@ open class InventoryService( | |||
| val inventory = inventoryRepository.findAll() | |||
| 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) | |||
| open fun updateInventory(request: SaveInventoryRequest): MessageResponse { | |||
| // out need id | |||
| // 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 | |||
| val inventory = inventoryRepository.findById(request.id).orElseThrow() | |||
| val newStatus = request.status ?: inventory.status | |||
| 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 { | |||
| qty = inventory.qty!! + reqQty | |||
| expiryDate = newExpiry | |||
| @@ -64,15 +71,10 @@ open class InventoryService( | |||
| val args = mapOf( | |||
| "from" to from, | |||
| "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 count = jdbcDao.queryForInt(sql.toString(), args) | |||
| val count = jdbcDao.queryForInt(INVENTORY_COUNT.toString(), args) | |||
| val newLotNo = CodeGenerator.generateCode(prefix, item.id!!, count) | |||
| val newExpiry = request.expiryDate | |||
| 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); | |||