feat: Add OnComplete functionality to Inventory Receipt

- Implemented OnComplete method in InventoryReceiptController to handle completion of inventory receipts.
- Added success and failure messages in dto for completing inventory receipts.
- Updated routes to include the new OnComplete endpoint.
- Enhanced InventoryReceiptService to manage the completion logic, including updating inventory storage.
- Introduced new methods in InventoryStorageRepository for bulk creation and retrieval of inventory storages.
- Added methods in ProductService to fetch inventory storages and transactions by product and client.
- Updated ProductController to handle new endpoints for inventory storages and transactions.
- Enhanced Product DTO to include inventory storage and transaction responses.
- Refactored ProductRepository to retrieve cross-references and preloaded relationships for inventory storages and transactions.
- Updated core dependency injection to include new repositories and services.
This commit is contained in:
Habib Fatkhul Rohman 2025-11-25 15:57:08 +07:00
parent e22c76abe3
commit 2ef6052a2c
16 changed files with 521 additions and 159 deletions

View File

@ -22,6 +22,7 @@ type InventoryReceiptController interface {
CreateLine(ctx *gin.Context)
UpdateLine(ctx *gin.Context)
DeleteLine(ctx *gin.Context)
OnComplete(ctx *gin.Context)
}
type inventoryReceiptController struct {
@ -29,6 +30,19 @@ type inventoryReceiptController struct {
db *gorm.DB
}
// OnComplete implements InventoryReceiptController.
func (c *inventoryReceiptController) OnComplete(ctx *gin.Context) {
id := ctx.Param("id")
updated, err := c.receiptService.OnComplete(ctx, id)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_COMPLETE_INVENTORY_RECEIPT, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_COMPLETE_INVENTORY_RECEIPT, updated)
ctx.JSON(http.StatusOK, res)
}
// DeleteLine implements InventoryReceiptController.
func (c *inventoryReceiptController) DeleteLine(ctx *gin.Context) {
id := ctx.Param("id")

View File

@ -18,6 +18,8 @@ const (
MESSAGE_SUCCESS_DELETE_INVENTORY_RECEIPT = "success delete inventory receipt"
MESSAGE_SUCCESS_DELETE_INVENTORY_RECEIPT_LINE = "success delete inventory receipt line"
MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body"
MESSAGE_SUCCESS_COMPLETE_INVENTORY_RECEIPT = "success complete inventory receipt"
MESSAGE_FAILED_COMPLETE_INVENTORY_RECEIPT = "failed complete inventory receipt"
)
type InventoryReceiptCreateRequest struct {
@ -34,10 +36,10 @@ type InventoryReceiptLineCreateRequest struct {
Quantity float64 `json:"quantity"`
BatchNumber string `json:"batch_number"`
RepackingSuggestion string `json:"repacking_suggestion"`
RepackUomID string `json:"repack_uom_id"`
RepackUomCode string `json:"repack_uom_code"`
ProductID string `json:"product_id"`
ProductCode string `json:"product_code"`
RepackUomID string `json:"repack_uom_id"`
RepackUomCode string `json:"repack_uom_code"`
ProductID string `json:"product_id"`
ProductCode string `json:"product_code"`
ClientID string `json:"client_id"`
}

View File

@ -23,5 +23,6 @@ func RegisterRoutes(server *gin.Engine, injector *do.Injector) {
receiptRoutes.POST(":id/lines", middlewares.Authenticate(jwtService), receiptController.CreateLine)
receiptRoutes.PUT("lines/:id", middlewares.Authenticate(jwtService), receiptController.UpdateLine)
receiptRoutes.DELETE("lines/:id", middlewares.Authenticate(jwtService), receiptController.DeleteLine)
receiptRoutes.POST(":id/complete", middlewares.Authenticate(jwtService), receiptController.OnComplete)
}
}

View File

@ -2,13 +2,16 @@ package service
import (
"context"
"errors"
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
dtodomain "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/dto"
"github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/query"
"github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/repository"
invstoragerepository "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_storage/repository"
productrepository "github.com/Caknoooo/go-gin-clean-starter/modules/product/repository"
uomrepository "github.com/Caknoooo/go-gin-clean-starter/modules/uom/repository"
"github.com/Caknoooo/go-gin-clean-starter/pkg/constants"
pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto"
"github.com/Caknoooo/go-gin-clean-starter/pkg/utils"
"github.com/google/uuid"
@ -24,14 +27,75 @@ type InventoryReceiptService interface {
CreateLine(ctx context.Context, receiptId string, req dtodomain.InventoryReceiptLineCreateRequest) (dtodomain.InventoryReceiptLineResponse, error)
UpdateLine(ctx context.Context, lineId string, req dtodomain.InventoryReceiptLineUpdateRequest) (dtodomain.InventoryReceiptLineResponse, error)
DeleteLine(ctx context.Context, lineId string) error
OnComplete(ctx context.Context, id string) (dtodomain.InventoryReceiptResponse, error)
}
type inventoryReceiptService struct {
db *gorm.DB
receiptRepo repository.InventoryReceiptRepository
receiptLineRepo repository.InventoryReceiptLineRepository
productRepo productrepository.ProductRepository
uomRepo uomrepository.UomRepository
db *gorm.DB
receiptRepo repository.InventoryReceiptRepository
receiptLineRepo repository.InventoryReceiptLineRepository
productRepo productrepository.ProductRepository
uomRepo uomrepository.UomRepository
invStorageRepository invstoragerepository.InventoryStorageRepository
}
func (s *inventoryReceiptService) OnComplete(ctx context.Context, id string) (dtodomain.InventoryReceiptResponse, error) {
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
receipt, err := s.receiptRepo.GetById(ctx, tx, id)
if err != nil {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
receipt.Status = constants.COMPLETED
for _, line := range receipt.ReceiptLines {
product, err := s.productRepo.GetById(ctx, tx, line.ProductID.String())
if err != nil {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
existing, err := s.invStorageRepository.GetByProductAndClient(ctx, tx, product.ID.String(), receipt.ClientID.String())
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
if existing.ID != uuid.Nil {
existing.OnHandQuantity += line.Quantity
existing.AvailableQuantity += line.Quantity
_, err = s.invStorageRepository.Update(ctx, tx, existing)
if err != nil {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
} else {
newStorage := entities.InventoryStorageEntity{
ProductID: line.ProductID,
UomID: *product.UomID,
OnHandQuantity: line.Quantity,
AvailableQuantity: line.Quantity,
InvReceiptID: receipt.ID,
ClientID: receipt.ClientID,
}
_, err = s.invStorageRepository.Create(ctx, tx, newStorage)
if err != nil {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
}
}
if _, err := s.receiptRepo.Update(ctx, tx, receipt); err != nil {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
tx.Commit()
return toInventoryReceiptResponse(receipt), nil
}
// DeleteLine implements InventoryReceiptService.
@ -235,6 +299,7 @@ func (s *inventoryReceiptService) Create(ctx context.Context, req dtodomain.Inve
}
// Bulk create lines
var lines []entities.TInventoryReceiptLineEntity
// var invStorages []entities.InventoryStorageEntity
for _, lineReq := range req.ReceiptLines {
var productUUID uuid.UUID
if lineReq.ProductID != "" {
@ -285,6 +350,21 @@ func (s *inventoryReceiptService) Create(ctx context.Context, req dtodomain.Inve
ProductID: productUUID,
ClientID: clientLineUUID,
})
// // Prepare inventory storage entity
// product, err := s.productRepo.GetById(ctx, tx, productUUID.String())
// if err != nil {
// tx.Rollback()
// return dtodomain.InventoryReceiptResponse{}, err
// }
// invStorages = append(invStorages, entities.InventoryStorageEntity{
// ProductID: productUUID,
// UomID: *product.UomID,
// OnHandQuantity: 0,
// AvailableQuantity: 0,
// InvReceiptID: created.ID,
// ClientID: clientUUID,
// })
}
if len(lines) > 0 {
err = s.receiptLineRepo.BulkCreate(ctx, tx, lines)
@ -293,6 +373,13 @@ func (s *inventoryReceiptService) Create(ctx context.Context, req dtodomain.Inve
return dtodomain.InventoryReceiptResponse{}, err
}
}
// if len(invStorages) > 0 {
// err = s.invStorageRepository.BulkCreate(ctx, tx, invStorages)
// if err != nil {
// tx.Rollback()
// return dtodomain.InventoryReceiptResponse{}, err
// }
// }
tx.Commit()
result, err := s.receiptRepo.GetById(ctx, nil, created.ID.String())
if err != nil {
@ -453,12 +540,14 @@ func NewInventoryReceiptService(db *gorm.DB,
receiptRepo repository.InventoryReceiptRepository,
receiptLineRepo repository.InventoryReceiptLineRepository,
productRepo productrepository.ProductRepository,
uomRepo uomrepository.UomRepository) InventoryReceiptService {
uomRepo uomrepository.UomRepository,
invStorageRepository invstoragerepository.InventoryStorageRepository) InventoryReceiptService {
return &inventoryReceiptService{
db: db,
receiptRepo: receiptRepo,
receiptLineRepo: receiptLineRepo,
productRepo: productRepo,
uomRepo: uomRepo,
db: db,
receiptRepo: receiptRepo,
receiptLineRepo: receiptLineRepo,
productRepo: productRepo,
uomRepo: uomRepo,
invStorageRepository: invStorageRepository,
}
}

View File

@ -75,10 +75,11 @@ type AssignmentUserResponse struct {
}
type InventoryRequestLineResponse struct {
ID string `json:"id"`
Quantity float64 `json:"quantity"`
Product InventoryRequestLineProductResponse `json:"product"`
ClientID string `json:"client_id"`
ID string `json:"id"`
Quantity float64 `json:"quantity"`
CurrentStock float64 `json:"current_stock"`
Product InventoryRequestLineProductResponse `json:"product"`
ClientID string `json:"client_id"`
}
type InventoryRequestLineProductResponse struct {

View File

@ -121,6 +121,7 @@ func toInventoryRequestResponse(e entities.TInventoryRequestEntity) dtodomain.In
lines = append(lines, dtodomain.InventoryRequestLineResponse{
ID: line.ID.String(),
Quantity: line.Quantity,
// CurrentStock: line.Product.CurrentStock,
Product: product,
ClientID: line.ClientID.String(),
})

View File

@ -33,8 +33,8 @@ type InventoryStorageCreateRequest struct {
ClientID string `json:"client_id" binding:"required"`
OnHandQuantity float64 `json:"on_hand_quantity"`
AvailableQuantity float64 `json:"available_quantity"`
InvRequestID string `json:"inv_request_id"`
InvReceiptID string `json:"inv_receipt_id"`
InvRequestID *string `json:"inv_request_id"`
InvReceiptID *string `json:"inv_receipt_id"`
}
type InventoryStorageUpdateRequest struct {

View File

@ -6,6 +6,7 @@ import (
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
"github.com/Caknoooo/go-gin-clean-starter/modules/inventory_storage/query"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type InventoryStorageRepository interface {
@ -14,12 +15,66 @@ type InventoryStorageRepository interface {
GetAll(ctx context.Context, filter query.InventoryStorageFilter) ([]entities.InventoryStorageEntity, int64, error)
Update(ctx context.Context, tx *gorm.DB, inventoryStorage entities.InventoryStorageEntity) (entities.InventoryStorageEntity, error)
Delete(ctx context.Context, tx *gorm.DB, inventoryStorageId string) error
BulkCreate(ctx context.Context, tx *gorm.DB, inventoryStorages []entities.InventoryStorageEntity) error
GetByProductAndClient(ctx context.Context, tx *gorm.DB, productId string, clientId string) (entities.InventoryStorageEntity, error)
GetStoragesByProductAndClient(ctx context.Context, tx *gorm.DB, productId string, clientId string) ([]entities.InventoryStorageEntity, error)
}
type inventoryStorageRepository struct {
db *gorm.DB
}
// GetStoragesByProductAndClient implements InventoryStorageRepository.
func (r *inventoryStorageRepository) GetStoragesByProductAndClient(ctx context.Context, tx *gorm.DB, productId string, clientId string) ([]entities.InventoryStorageEntity, error) {
if tx == nil {
tx = r.db
}
var inventoryStorages []entities.InventoryStorageEntity
if err := tx.WithContext(ctx).
Preload("Product").
Preload("Aisle").
Preload("Uom").
Preload("Client").
Preload("InvRequest").
Preload("InvReceipt").
Where("product_id = ? AND client_id = ?", productId, clientId).
Find(&inventoryStorages).Error; err != nil {
return nil, err
}
return inventoryStorages, nil
}
// GetByProductAndClient implements InventoryStorageRepository.
func (r *inventoryStorageRepository) GetByProductAndClient(ctx context.Context, tx *gorm.DB, productId string, clientId string) (entities.InventoryStorageEntity, error) {
if tx == nil {
tx = r.db
}
var inventoryStorage entities.InventoryStorageEntity
if err := tx.WithContext(ctx).
Preload("Product").
Preload("Aisle").
Preload("Uom").
Preload("Client").
Preload("InvRequest").
Preload("InvReceipt").
Order("created_at DESC").
First(&inventoryStorage, "product_id = ? AND client_id = ?", productId, clientId).Error; err != nil {
return inventoryStorage, err
}
return inventoryStorage, nil
}
// BulkCreate implements InventoryStorageRepository.
func (r *inventoryStorageRepository) BulkCreate(ctx context.Context, tx *gorm.DB, inventoryStorages []entities.InventoryStorageEntity) error {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(&inventoryStorages).Error; err != nil {
return err
}
return nil
}
func NewInventoryStorageRepository(db *gorm.DB) InventoryStorageRepository {
return &inventoryStorageRepository{db: db}
}

View File

@ -51,16 +51,26 @@ func (s *inventoryStorageService) Create(ctx context.Context, req dtodomain.Inve
tx.Rollback()
return dtodomain.InventoryStorageResponse{}, err
}
InvRequestID, err := uuid.Parse(req.InvRequestID)
if err != nil {
tx.Rollback()
return dtodomain.InventoryStorageResponse{}, err
var InvRequestID, InvReceiptID uuid.UUID
if req.InvRequestID != nil && *req.InvRequestID != "" {
InvRequestID, err = uuid.Parse(*req.InvRequestID)
if err != nil {
tx.Rollback()
return dtodomain.InventoryStorageResponse{}, err
}
} else {
InvRequestID = uuid.Nil
}
InvReceiptID, err := uuid.Parse(req.InvReceiptID)
if err != nil {
tx.Rollback()
return dtodomain.InventoryStorageResponse{}, err
if req.InvReceiptID != nil && *req.InvReceiptID != "" {
InvReceiptID, err = uuid.Parse(*req.InvReceiptID)
if err != nil {
tx.Rollback()
return dtodomain.InventoryStorageResponse{}, err
}
} else {
InvReceiptID = uuid.Nil
}
inventoryStorage := entities.InventoryStorageEntity{
ProductID: productID,
AisleID: aisleID,

View File

@ -14,12 +14,33 @@ type InventoryTransactionRepository interface {
GetAll(ctx context.Context, filter query.InventoryTransactionFilter) ([]entities.InventoryTransactionEntity, int64, error)
Update(ctx context.Context, tx *gorm.DB, inventoryTransaction entities.InventoryTransactionEntity) (entities.InventoryTransactionEntity, error)
Delete(ctx context.Context, tx *gorm.DB, inventoryTransactionId string) error
GetByProductAndClient(ctx context.Context, tx *gorm.DB, productId string, clientId string) ([]entities.InventoryTransactionEntity, error)
}
type inventoryTransactionRepository struct {
db *gorm.DB
}
// GetByProductAndClient implements InventoryTransactionRepository.
func (r *inventoryTransactionRepository) GetByProductAndClient(ctx context.Context, tx *gorm.DB, productId string, clientId string) ([]entities.InventoryTransactionEntity, error) {
if tx == nil {
tx = r.db
}
var inventoryTransactions []entities.InventoryTransactionEntity
if err := tx.WithContext(ctx).
Preload("Client").
Preload("Product").
Preload("Aisle").
Preload("InvReceipt").
Preload("InvIssue").
Preload("InvMove").
Where("product_id = ? AND client_id = ?", productId, clientId).
Find(&inventoryTransactions).Error; err != nil {
return nil, err
}
return inventoryTransactions, nil
}
func NewInventoryTransactionRepository(db *gorm.DB) InventoryTransactionRepository {
return &inventoryTransactionRepository{db: db}
}

View File

@ -22,6 +22,9 @@ type (
GetAll(ctx *gin.Context)
AssignCrossReference(ctx *gin.Context)
RemoveCrossReference(ctx *gin.Context)
GetInvStoragesByProductAndClient(ctx *gin.Context)
GetInvTransactionsByProductAndClient(ctx *gin.Context)
GetCrossReferencesByProductAndClient(ctx *gin.Context)
}
productController struct {
@ -30,6 +33,49 @@ type (
}
)
// GetCrossReferencesByProductAndClient implements ProductController.
func (c *productController) GetCrossReferencesByProductAndClient(ctx *gin.Context) {
id := ctx.Param("id")
crossReferences, err := c.productService.GetCrossReferencesByProduct(ctx, id)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_CROSS_REFERENCES, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_CROSS_REFERENCES, crossReferences)
ctx.JSON(http.StatusOK, res)
}
// GetInvStoragesByProductAndClient implements ProductController.
func (c *productController) GetInvStoragesByProductAndClient(ctx *gin.Context) {
id := ctx.Param("id")
clientId := ctx.MustGet("client_id").(string)
invStorages, err := c.productService.GetInvStoragesByProductAndClient(ctx, id, clientId)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_INV_STORAGES, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_INV_STORAGES, invStorages)
ctx.JSON(http.StatusOK, res)
}
// GetInvTransactionsByProductAndClient implements ProductController.
func (c *productController) GetInvTransactionsByProductAndClient(ctx *gin.Context) {
id := ctx.Param("id")
clientId := ctx.MustGet("client_id").(string)
invTransactions, err := c.productService.GetInvTransactionsByProductAndClient(ctx, id, clientId)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_INV_TRANSACTIONS, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_INV_TRANSACTIONS, invTransactions)
ctx.JSON(http.StatusOK, res)
}
func NewProductController(i *do.Injector, productService service.ProductService) ProductController {
db := do.MustInvokeNamed[*gorm.DB](i, constants.DB)
return &productController{

View File

@ -11,19 +11,25 @@ import (
)
const (
MESSAGE_FAILED_CREATE_PRODUCT = "failed create product"
MESSAGE_SUCCESS_CREATE_PRODUCT = "success create product"
MESSAGE_FAILED_GET_PRODUCT = "failed get product"
MESSAGE_SUCCESS_GET_PRODUCT = "success get product"
MESSAGE_FAILED_UPDATE_PRODUCT = "failed update product"
MESSAGE_SUCCESS_UPDATE_PRODUCT = "success update product"
MESSAGE_FAILED_DELETE_PRODUCT = "failed delete product"
MESSAGE_SUCCESS_DELETE_PRODUCT = "success delete product"
MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body"
MESSAGE_FAILED_ASSIGN_CROSS_REF = "failed assign cross reference"
MESSAGE_SUCCESS_ASSIGN_CROSS_REF = "success assign cross reference"
MESSAGE_FAILED_REMOVE_CROSS_REF = "failed remove cross reference"
MESSAGE_SUCCESS_REMOVE_CROSS_REF = "success remove cross reference"
MESSAGE_FAILED_CREATE_PRODUCT = "failed create product"
MESSAGE_SUCCESS_CREATE_PRODUCT = "success create product"
MESSAGE_FAILED_GET_PRODUCT = "failed get product"
MESSAGE_SUCCESS_GET_PRODUCT = "success get product"
MESSAGE_FAILED_UPDATE_PRODUCT = "failed update product"
MESSAGE_SUCCESS_UPDATE_PRODUCT = "success update product"
MESSAGE_FAILED_DELETE_PRODUCT = "failed delete product"
MESSAGE_SUCCESS_DELETE_PRODUCT = "success delete product"
MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body"
MESSAGE_FAILED_ASSIGN_CROSS_REF = "failed assign cross reference"
MESSAGE_SUCCESS_ASSIGN_CROSS_REF = "success assign cross reference"
MESSAGE_FAILED_REMOVE_CROSS_REF = "failed remove cross reference"
MESSAGE_SUCCESS_REMOVE_CROSS_REF = "success remove cross reference"
MESSAGE_FAILED_GET_INV_STORAGES = "failed get inventory storages by product and client"
MESSAGE_SUCCESS_GET_INV_STORAGES = "success get inventory storages by product and client"
MESSAGE_FAILED_GET_INV_TRANSACTIONS = "failed get inventory transactions by product and client"
MESSAGE_SUCCESS_GET_INV_TRANSACTIONS = "success get inventory transactions by product and client"
MESSAGE_SUCCESS_GET_CROSS_REFERENCES = "success get cross references by product and client"
MESSAGE_FAILED_GET_CROSS_REFERENCES = "failed get cross references by product and client"
)
var (
@ -107,41 +113,42 @@ type (
}
ProductResponse struct {
ID string `json:"id"`
Name string `json:"name"`
RefNumber string `json:"ref_number"`
SKU string `json:"sku"`
Description string `json:"description"`
Status string `json:"status"`
IsReturnable bool `json:"is_returnable"`
DimLength float64 `json:"dim_length"`
DimWidth float64 `json:"dim_width"`
DimHeight float64 `json:"dim_height"`
Weight float64 `json:"weight"`
Volume float64 `json:"volume"`
MaxStackHeight int `json:"max_stack_height"`
Temperature string `json:"temperature"`
IsHazardous bool `json:"is_hazardous"`
MinStock int `json:"min_stock"`
MaxStock int `json:"max_stock"`
ReplenishType string `json:"replenish_type"`
CycleCount string `json:"cycle_count"`
LotRules string `json:"lot_rules"`
LeadTime int `json:"lead_time"`
MultiplyRate string `json:"multiply_rate"`
DivideRate float64 `json:"divide_rate"`
Client pkgdto.IdNameResponse `json:"client"`
Category pkgdto.IdNameResponse `json:"category"`
Uom pkgdto.IdNameResponse `json:"uom"`
DimUom pkgdto.IdNameResponse `json:"dim_uom"`
WeightUom pkgdto.IdNameResponse `json:"weight_uom"`
VolumeUom pkgdto.IdNameResponse `json:"volume_uom"`
MinStockUom pkgdto.IdNameResponse `json:"min_stock_uom"`
MaxStockUom pkgdto.IdNameResponse `json:"max_stock_uom"`
LeadTimeUom pkgdto.IdNameResponse `json:"lead_time_uom"`
UomToUom pkgdto.IdNameResponse `json:"uom_to_uom"`
CrossReferences []ProductVendorResponse `json:"cross_references"`
InvTransactions []ProductInventoryTransactionResponse `json:"inv_transactions"`
ID string `json:"id"`
Name string `json:"name"`
RefNumber string `json:"ref_number"`
SKU string `json:"sku"`
Description string `json:"description"`
Status string `json:"status"`
IsReturnable bool `json:"is_returnable"`
DimLength float64 `json:"dim_length"`
DimWidth float64 `json:"dim_width"`
DimHeight float64 `json:"dim_height"`
Weight float64 `json:"weight"`
Volume float64 `json:"volume"`
MaxStackHeight int `json:"max_stack_height"`
Temperature string `json:"temperature"`
IsHazardous bool `json:"is_hazardous"`
MinStock int `json:"min_stock"`
MaxStock int `json:"max_stock"`
ReplenishType string `json:"replenish_type"`
CycleCount string `json:"cycle_count"`
LotRules string `json:"lot_rules"`
LeadTime int `json:"lead_time"`
MultiplyRate string `json:"multiply_rate"`
DivideRate float64 `json:"divide_rate"`
Client pkgdto.IdNameResponse `json:"client"`
Category pkgdto.IdNameResponse `json:"category"`
Uom pkgdto.IdNameResponse `json:"uom"`
DimUom pkgdto.IdNameResponse `json:"dim_uom"`
WeightUom pkgdto.IdNameResponse `json:"weight_uom"`
VolumeUom pkgdto.IdNameResponse `json:"volume_uom"`
MinStockUom pkgdto.IdNameResponse `json:"min_stock_uom"`
MaxStockUom pkgdto.IdNameResponse `json:"max_stock_uom"`
LeadTimeUom pkgdto.IdNameResponse `json:"lead_time_uom"`
UomToUom pkgdto.IdNameResponse `json:"uom_to_uom"`
// CrossReferences []ProductVendorResponse `json:"cross_references"`
// InvTransactions []ProductInventoryTransactionResponse `json:"inv_transactions"`
// InvStorages []ProductInventoryStorageResponse `json:"inv_storages"`
}
CrossReferenceRequest struct {
@ -167,82 +174,21 @@ type (
InvIssueRef string `json:"inv_issue_ref,omitempty"`
InvMoveRef string `json:"inv_move_ref,omitempty"`
}
ProductInventoryStorageResponse struct {
ID string `json:"id"`
Locater string `json:"locater"`
Lot string `json:"lot"`
OnHandQuantity float64 `json:"on_hand_quantity"`
AvailableQuantity float64 `json:"available_quantity"`
AisleID pkgdto.IdNameResponse `json:"aisle_id"`
Uom pkgdto.IdNameResponse `json:"uom"`
InvReceipt pkgdto.IdNameResponse `json:"inv_receipt"`
InvRequest pkgdto.IdNameResponse `json:"inv_request"`
}
)
func MapProductToResponse(product entities.MProductEntity) ProductResponse {
crossRefs := make([]ProductVendorResponse, 0, len(product.CrossReferences))
for _, v := range product.CrossReferences {
crossRefs = append(crossRefs, ProductVendorResponse{
ID: v.Vendor.ID.String(),
Name: v.Vendor.Name,
Address: v.Vendor.Address,
ContactPerson: v.Vendor.ContactPerson,
SearchKey: v.Vendor.SearchKey,
})
}
invTransactions := make([]ProductInventoryTransactionResponse, 0, len(product.InventoryTransactions))
for _, it := range product.InventoryTransactions {
var transactionQuantity float64
var lot, locater, invReceiptRef, invIssueRef, invMoveRef string
var transactionDate string
valid := false
// Receipt
if it.InvReceipt.ID != uuid.Nil && it.InvReceipt.Status == constants.COMPLETED {
invReceiptRef = it.InvReceipt.ReferenceNumber
transactionDate = utils.DateTimeToString(it.TransactionDate)
for _, line := range it.InvReceipt.ReceiptLines {
if line.ProductID == it.ProductID {
transactionQuantity = line.Quantity
lot = line.BatchNumber
valid = true
break
}
}
}
// Issue
if it.InvIssue.ID != uuid.Nil && it.InvIssue.Status == constants.COMPLETED {
invIssueRef = it.InvIssue.DocumentNumber
transactionDate = utils.DateTimeToString(it.TransactionDate)
for _, line := range it.InvIssue.IssueLines {
if line.ProductID == it.ProductID {
transactionQuantity = line.IssuedQuantity
valid = true
break
}
}
}
// Move
if it.InvMove.ID != uuid.Nil && it.InvMove.Status == constants.COMPLETED {
invMoveRef = it.InvMove.MovementNumber
transactionDate = utils.DateTimeToString(it.TransactionDate)
for _, line := range it.InvMove.MovementLines {
if line.ProductID == it.ProductID {
transactionQuantity = line.MovedQuantity
valid = true
break
}
}
}
if valid {
invTransactions = append(invTransactions, ProductInventoryTransactionResponse{
ID: it.ID.String(),
TransactionDate: transactionDate,
TransactionType: it.TransactionType,
TransactionQuantity: transactionQuantity,
Lot: lot,
Locater: locater,
InvReceiptRef: invReceiptRef,
InvIssueRef: invIssueRef,
InvMoveRef: invMoveRef,
})
}
}
return ProductResponse{
ID: product.ID.String(),
Name: product.Name,
@ -307,7 +253,111 @@ func MapProductToResponse(product entities.MProductEntity) ProductResponse {
ID: product.UomToUom.ID.String(),
Name: product.UomToUom.Name,
},
CrossReferences: crossRefs,
InvTransactions: invTransactions,
}
}
func MapInventoryTransactionsToResponses(transactions []entities.InventoryTransactionEntity) []ProductInventoryTransactionResponse {
responses := make([]ProductInventoryTransactionResponse, 0, len(transactions))
for _, t := range transactions {
resp := MapInventoryTransactionToProductInventoryTransactionResponse(t)
if resp.ID != "" { // hanya tambahkan jika mapping valid
responses = append(responses, resp)
}
}
return responses
}
func MapInventoryTransactionToProductInventoryTransactionResponse(transaction entities.InventoryTransactionEntity) ProductInventoryTransactionResponse {
var transactionQuantity float64
var lot, locater, invReceiptRef, invIssueRef, invMoveRef string
var transactionDate string
valid := false
if transaction.InvReceipt.ID != uuid.Nil && transaction.InvReceipt.Status == constants.COMPLETED {
invReceiptRef = transaction.InvReceipt.ReferenceNumber
transactionDate = utils.DateTimeToString(transaction.TransactionDate)
for _, line := range transaction.InvReceipt.ReceiptLines {
if line.ProductID == transaction.ProductID {
transactionQuantity = line.Quantity
lot = line.BatchNumber
valid = true
break
}
}
}
if transaction.InvIssue.ID != uuid.Nil && transaction.InvIssue.Status == constants.COMPLETED {
invIssueRef = transaction.InvIssue.DocumentNumber
transactionDate = utils.DateTimeToString(transaction.TransactionDate)
for _, line := range transaction.InvIssue.IssueLines {
if line.ProductID == transaction.ProductID {
transactionQuantity = line.IssuedQuantity
valid = true
break
}
}
}
if transaction.InvMove.ID != uuid.Nil && transaction.InvMove.Status == constants.COMPLETED {
invMoveRef = transaction.InvMove.MovementNumber
transactionDate = utils.DateTimeToString(transaction.TransactionDate)
for _, line := range transaction.InvMove.MovementLines {
if line.ProductID == transaction.ProductID {
transactionQuantity = line.MovedQuantity
valid = true
break
}
}
}
if !valid {
return ProductInventoryTransactionResponse{}
}
return ProductInventoryTransactionResponse{
ID: transaction.ID.String(),
TransactionDate: transactionDate,
TransactionType: transaction.TransactionType,
TransactionQuantity: transactionQuantity,
Lot: lot,
Locater: locater,
InvReceiptRef: invReceiptRef,
InvIssueRef: invIssueRef,
InvMoveRef: invMoveRef,
}
}
func MapInventoryStorageToProductInventoryStorageResponse(storage entities.InventoryStorageEntity) ProductInventoryStorageResponse {
return ProductInventoryStorageResponse{
ID: storage.ID.String(),
// Locater: storage.Locater,
// Lot: storage.BatchNumber,
OnHandQuantity: storage.OnHandQuantity,
AvailableQuantity: storage.AvailableQuantity,
AisleID: pkgdto.IdNameResponse{
ID: storage.Aisle.ID.String(),
Name: storage.Aisle.Name,
},
Uom: pkgdto.IdNameResponse{
ID: storage.Uom.ID.String(),
Name: storage.Uom.Name,
},
InvReceipt: pkgdto.IdNameResponse{
ID: storage.InvReceipt.ID.String(),
Name: storage.InvReceipt.ReferenceNumber,
},
InvRequest: pkgdto.IdNameResponse{
ID: storage.InvRequest.ID.String(),
Name: storage.InvRequest.ReferenceNumber,
},
}
}
func MapCrossReferenceToProductVendorResponse(crossRef entities.MCrossReferenceEntity) ProductVendorResponse {
return ProductVendorResponse{
ID: crossRef.Vendor.ID.String(),
Name: crossRef.Vendor.Name,
ContactPerson: crossRef.Vendor.ContactPerson,
SearchKey: crossRef.Vendor.SearchKey,
Address: crossRef.Vendor.Address}
}

View File

@ -19,12 +19,27 @@ type ProductRepository interface {
Delete(ctx context.Context, tx *gorm.DB, productId string) error
AssignCrossReference(ctx context.Context, tx *gorm.DB, productId string, vendorIds []string) error
RemoveCrossReference(ctx context.Context, tx *gorm.DB, productId string, vendorIds []string) error
GetCrossReferencesByProduct(ctx context.Context, tx *gorm.DB, productId string) ([]entities.MCrossReferenceEntity, error)
}
type productRepository struct {
db *gorm.DB
}
// GetCrossReferencesByProduct implements ProductRepository.
func (r *productRepository) GetCrossReferencesByProduct(ctx context.Context, tx *gorm.DB, productId string) ([]entities.MCrossReferenceEntity, error) {
if tx == nil {
tx = r.db
}
var crossReferences []entities.MCrossReferenceEntity
if err := tx.WithContext(ctx).
Where("product_id = ?", productId).
Find(&crossReferences).Error; err != nil {
return nil, err
}
return crossReferences, nil
}
// GetByCode implements ProductRepository.
func (r *productRepository) GetByCode(ctx context.Context, tx *gorm.DB, productCode string, clientId string) (entities.MProductEntity, error) {
if tx == nil {
@ -156,6 +171,12 @@ func (r *productRepository) GetById(ctx context.Context, tx *gorm.DB, productId
Preload("CrossReferences").
Preload("CrossReferences.Vendor").
Preload("InventoryStorages").
Preload("InventoryStorages.Client").
Preload("InventoryStorages.Product").
Preload("InventoryStorages.Aisle").
Preload("InventoryStorages.Uom").
Preload("InventoryStorages.InvReceipt").
Preload("InventoryStorages.InvRequest").
Preload("InventoryTransactions").
Preload("InventoryTransactions.Client").
Preload("InventoryTransactions.Aisle").

View File

@ -22,5 +22,8 @@ func RegisterRoutes(server *gin.Engine, injector *do.Injector) {
productRoutes.GET("", middlewares.Authenticate(jwtService), productController.GetAll)
productRoutes.POST("/:id/assign-cross-reference", middlewares.Authenticate(jwtService), productController.AssignCrossReference)
productRoutes.POST("/:id/remove-cross-reference", middlewares.Authenticate(jwtService), productController.RemoveCrossReference)
productRoutes.GET("/:id/inv-storages", middlewares.Authenticate(jwtService), productController.GetInvStoragesByProductAndClient)
productRoutes.GET("/:id/inv-transactions", middlewares.Authenticate(jwtService), productController.GetInvTransactionsByProductAndClient)
productRoutes.GET("/:id/cross-references", middlewares.Authenticate(jwtService), productController.GetCrossReferencesByProductAndClient)
}
}

View File

@ -4,6 +4,8 @@ import (
"context"
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
invstoragerepo "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_storage/repository"
invtransactionrepo "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_transaction/repository"
"github.com/Caknoooo/go-gin-clean-starter/modules/product/dto"
"github.com/Caknoooo/go-gin-clean-starter/modules/product/query"
"github.com/Caknoooo/go-gin-clean-starter/modules/product/repository"
@ -19,11 +21,55 @@ type ProductService interface {
Delete(ctx context.Context, productId string) error
AssignCrossReference(ctx context.Context, productId string, vendorIds []string) error
RemoveCrossReference(ctx context.Context, productId string, vendorIds []string) error
GetInvTransactionsByProductAndClient(ctx context.Context, productId string, clientId string) ([]dto.ProductInventoryTransactionResponse, error)
GetInvStoragesByProductAndClient(ctx context.Context, productId string, clientId string) ([]dto.ProductInventoryStorageResponse, error)
GetCrossReferencesByProduct(ctx context.Context, productId string) ([]dto.ProductVendorResponse, error)
}
type productService struct {
db *gorm.DB
productRepo repository.ProductRepository
db *gorm.DB
productRepo repository.ProductRepository
inventoryTransactionRepo invtransactionrepo.InventoryTransactionRepository
inventoryStorageRepo invstoragerepo.InventoryStorageRepository
}
// GetCrossReferencesByProductAndClient implements ProductService.
func (s *productService) GetCrossReferencesByProduct(ctx context.Context, productId string) ([]dto.ProductVendorResponse, error) {
crossReferences, err := s.productRepo.GetCrossReferencesByProduct(ctx, nil, productId)
if err != nil {
return nil, err
}
var responses []dto.ProductVendorResponse
for _, cr := range crossReferences {
responses = append(responses, dto.MapCrossReferenceToProductVendorResponse(cr))
}
if len(responses) == 0 {
responses = []dto.ProductVendorResponse{} // pastikan slice kosong, bukan nil
}
return responses, nil
}
// GetInvStoragesByProductAndClient implements ProductService.
func (s *productService) GetInvStoragesByProductAndClient(ctx context.Context, productId string, clientId string) ([]dto.ProductInventoryStorageResponse, error) {
invStorages, err := s.inventoryStorageRepo.GetStoragesByProductAndClient(ctx, nil, productId, clientId)
if err != nil {
return nil, err
}
var responses []dto.ProductInventoryStorageResponse
for _, invStorage := range invStorages {
responses = append(responses, dto.MapInventoryStorageToProductInventoryStorageResponse(invStorage))
}
return responses, nil
}
// GetInvTransactionsByProductAndClient implements ProductService.
func (s *productService) GetInvTransactionsByProductAndClient(ctx context.Context, productId string, clientId string) ([]dto.ProductInventoryTransactionResponse, error) {
invTransactions, err := s.inventoryTransactionRepo.GetByProductAndClient(ctx, nil, productId, clientId)
if err != nil {
return nil, err
}
responses := dto.MapInventoryTransactionsToResponses(invTransactions)
return responses, nil
}
// AssignCrossReference implements ProductService.
@ -66,10 +112,12 @@ func (s *productService) RemoveCrossReference(ctx context.Context, productId str
return nil
}
func NewProductService(productRepo repository.ProductRepository, db *gorm.DB) ProductService {
func NewProductService(productRepo repository.ProductRepository, db *gorm.DB, inventoryTransactionRepo invtransactionrepo.InventoryTransactionRepository, inventoryStorageRepo invstoragerepo.InventoryStorageRepository) ProductService {
return &productService{
productRepo: productRepo,
db: db,
productRepo: productRepo,
db: db,
inventoryTransactionRepo: inventoryTransactionRepo,
inventoryStorageRepo: inventoryStorageRepo,
}
}

View File

@ -162,7 +162,7 @@ func RegisterDependencies(injector *do.Injector) {
// Service
userServ := userService.NewUserService(userRepository, roleRepository, warehouseRepository, clientRepository, refreshTokenRepository, jwtService, db)
productService := productService.NewProductService(productRepository, db)
productService := productService.NewProductService(productRepository, db, inventoryTransactionRepository, inventoryStorageRepository)
roleServ := roleService.NewRoleService(roleRepository, refreshTokenRepository, jwtService, userServ, db)
menuSvc := menuService.NewMenuService(menuRepository, jwtService, db)
maintenanceGroupServ := maintGroupService.NewMaintenanceGroupService(maintenanceGroupRepository, maintenanceGroupRoleRepository, maintenanceGroupRoleUserRepository, db)
@ -174,7 +174,7 @@ func RegisterDependencies(injector *do.Injector) {
warehouseServ := warehouseService.NewWarehouseService(warehouseRepository, db)
zonaServ := zonaService.NewZonaService(zonaRepository, db)
aisleServ := aisleService.NewAisleService(aisleRepository, db)
inventoryReceiptServ := inventoryReceiptService.NewInventoryReceiptService(db, inventoryReceiptRepository, inventoryReceiptLineRepository, productRepository, uomRepository)
inventoryReceiptServ := inventoryReceiptService.NewInventoryReceiptService(db, inventoryReceiptRepository, inventoryReceiptLineRepository, productRepository, uomRepository, inventoryStorageRepository)
assignmentServ := assignmentService.NewAssignmentService(db, assignmentRepository, assignmentUserRepository)
inventoryRequestServ := inventoryRequestService.NewInventoryRequestService(db, inventoryRequestRepository, inventoryRequestLineRepository)
inventoryIssueServ := inventoryIssueService.NewInventoryIssueService(db, inventoryIssueRepository, inventoryIssueLineRepository)