feat: add inventory receipt module with CRUD operations

- Implemented InventoryReceiptController for handling inventory receipt requests.
- Created InventoryReceiptService for business logic related to inventory receipts.
- Added InventoryReceiptRepository and InventoryReceiptLineRepository for database interactions.
- Introduced DTOs for inventory receipt and line requests/responses.
- Implemented filtering for inventory receipts in the query layer.
- Added routes for inventory receipt operations in the API.
- Enhanced document number generation to include client initials.
- Updated database entities to support inventory receipts and lines.
- Added utility functions for date and time conversions.
This commit is contained in:
Habib Fatkhul Rohman 2025-11-14 13:14:32 +07:00
parent d29e2211cd
commit e90fa96dd6
12 changed files with 969 additions and 10 deletions

View File

@ -13,6 +13,7 @@ import (
"github.com/Caknoooo/go-gin-clean-starter/modules/auth" "github.com/Caknoooo/go-gin-clean-starter/modules/auth"
"github.com/Caknoooo/go-gin-clean-starter/modules/category" "github.com/Caknoooo/go-gin-clean-starter/modules/category"
"github.com/Caknoooo/go-gin-clean-starter/modules/client" "github.com/Caknoooo/go-gin-clean-starter/modules/client"
"github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt"
maintenancegroup "github.com/Caknoooo/go-gin-clean-starter/modules/maintenance_group" maintenancegroup "github.com/Caknoooo/go-gin-clean-starter/modules/maintenance_group"
"github.com/Caknoooo/go-gin-clean-starter/modules/menu" "github.com/Caknoooo/go-gin-clean-starter/modules/menu"
"github.com/Caknoooo/go-gin-clean-starter/modules/monitoring" "github.com/Caknoooo/go-gin-clean-starter/modules/monitoring"
@ -161,6 +162,7 @@ func main() {
warehouse.RegisterRoutes(server, injector) warehouse.RegisterRoutes(server, injector)
zona.RegisterRoutes(server, injector) zona.RegisterRoutes(server, injector)
aisle.RegisterRoutes(server, injector) aisle.RegisterRoutes(server, injector)
inventory_receipt.RegisterRoutes(server, injector)
// register swagger route // register swagger route
server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

View File

@ -19,7 +19,8 @@ type TInventoryReceiptEntity struct {
ClientID uuid.UUID `gorm:"type:uuid;index;" json:"client_id"` ClientID uuid.UUID `gorm:"type:uuid;index;" json:"client_id"`
Client M_Client `gorm:"foreignKey:ClientID;references:ID"` ReceiptLines []TInventoryReceiptLineEntity `gorm:"foreignKey:InvReceiptID;references:ID"`
Client M_Client `gorm:"foreignKey:ClientID;references:ID"`
FullAuditTrail FullAuditTrail
} }
@ -29,14 +30,30 @@ func (TInventoryReceiptEntity) TableName() string {
} }
// GenerateDocumentNumber generates a new document number for a client // GenerateDocumentNumber generates a new document number for a client
func GenerateDocumentNumber(db *gorm.DB, clientName string) (string, error) { func GenerateDocumentNumber(db *gorm.DB, clientId string) (string, error) {
prefix := "RCPT" prefix := "RCPT"
firstLetter := strings.ToUpper(string(clientName[0]))
// Ambil nama client berdasarkan clientId
var client struct {
Name string
}
if err := db.Table("m_clients").Select("name").Where("id = ?", clientId).First(&client).Error; err != nil {
return "", fmt.Errorf("client not found")
}
if client.Name == "" {
return "", fmt.Errorf("client name is empty")
}
words := strings.Fields(client.Name)
initials := ""
for _, w := range words {
if len(w) > 0 {
initials += strings.ToUpper(string(w[0]))
}
}
// Cari document number terakhir untuk client ini
var lastReceipt TInventoryReceiptEntity var lastReceipt TInventoryReceiptEntity
err := db. err := db.
Joins("JOIN m_clients ON m_clients.id = t_inventory_receipts.client_id"). Where("client_id = ?", clientId).
Where("m_clients.name = ?", clientName).
Order("document_number DESC"). Order("document_number DESC").
First(&lastReceipt).Error First(&lastReceipt).Error
@ -49,6 +66,6 @@ func GenerateDocumentNumber(db *gorm.DB, clientName string) (string, error) {
} }
} }
docNum := fmt.Sprintf("%s-%s-%04d", prefix, firstLetter, seq) docNum := fmt.Sprintf("%s-%s-%04d", prefix, initials, seq)
return docNum, nil return docNum, nil
} }

View File

@ -10,10 +10,10 @@ type TInventoryReceiptLineEntity struct {
BatchNumber string `gorm:"type:varchar(100);" json:"batch_number"` BatchNumber string `gorm:"type:varchar(100);" json:"batch_number"`
RepackingSuggestion string `gorm:"type:text;" json:"repacking_suggestion"` RepackingSuggestion string `gorm:"type:text;" json:"repacking_suggestion"`
RepackUomID uuid.UUID `gorm:"type:uuid;index;" json:"repack_uom_id"` RepackUomID *uuid.UUID `gorm:"type:uuid;index;" json:"repack_uom_id"`
InvReceiptID uuid.UUID `gorm:"type:uuid;index;" json:"inv_receipt_id"` InvReceiptID uuid.UUID `gorm:"type:uuid;index;" json:"inv_receipt_id"`
ProductID uuid.UUID `gorm:"type:uuid;index;" json:"product_id"` ProductID uuid.UUID `gorm:"type:uuid;index;" json:"product_id"`
ClientID uuid.UUID `gorm:"type:uuid;index;" json:"client_id"` ClientID uuid.UUID `gorm:"type:uuid;index;" json:"client_id"`
Product MProductEntity `gorm:"foreignKey:ProductID;references:ID"` Product MProductEntity `gorm:"foreignKey:ProductID;references:ID"`
RepackUom MUomEntity `gorm:"foreignKey:RepackUomID;references:ID"` RepackUom MUomEntity `gorm:"foreignKey:RepackUomID;references:ID"`

View File

@ -0,0 +1,182 @@
package controller
import (
"net/http"
"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/service"
"github.com/Caknoooo/go-gin-clean-starter/pkg/constants"
"github.com/Caknoooo/go-gin-clean-starter/pkg/utils"
"github.com/gin-gonic/gin"
"github.com/samber/do"
"gorm.io/gorm"
)
type InventoryReceiptController interface {
Create(ctx *gin.Context)
Update(ctx *gin.Context)
Delete(ctx *gin.Context)
GetById(ctx *gin.Context)
GetAll(ctx *gin.Context)
CreateLine(ctx *gin.Context)
UpdateLine(ctx *gin.Context)
DeleteLine(ctx *gin.Context)
}
type inventoryReceiptController struct {
receiptService service.InventoryReceiptService
db *gorm.DB
}
// DeleteLine implements InventoryReceiptController.
func (c *inventoryReceiptController) DeleteLine(ctx *gin.Context) {
id := ctx.Param("id")
if err := c.receiptService.DeleteLine(ctx, id); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_DELETE_INVENTORY_RECEIPT_LINE, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_DELETE_INVENTORY_RECEIPT_LINE, nil)
ctx.JSON(http.StatusOK, res)
}
// UpdateLine implements InventoryReceiptController.
func (c *inventoryReceiptController) UpdateLine(ctx *gin.Context) {
id := ctx.Param("id")
var req dto.InventoryReceiptLineUpdateRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
updated, err := c.receiptService.UpdateLine(ctx, id, req)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_UPDATE_INVENTORY_RECEIPT_LINE, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_UPDATE_INVENTORY_RECEIPT_LINE, updated)
ctx.JSON(http.StatusOK, res)
}
// CreateLine implements InventoryReceiptController.
func (c *inventoryReceiptController) CreateLine(ctx *gin.Context) {
id := ctx.Param("id")
var req dto.InventoryReceiptLineCreateRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
created, err := c.receiptService.CreateLine(ctx, id, req)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_INVENTORY_RECEIPT_LINE, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_CREATE_INVENTORY_RECEIPT_LINE, created)
ctx.JSON(http.StatusOK, res)
}
func (c *inventoryReceiptController) Create(ctx *gin.Context) {
var req dto.InventoryReceiptCreateRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
created, err := c.receiptService.Create(ctx, req)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_INVENTORY_RECEIPT, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_CREATE_INVENTORY_RECEIPT, created)
ctx.JSON(http.StatusOK, res)
}
func (c *inventoryReceiptController) Update(ctx *gin.Context) {
id := ctx.Param("id")
var req dto.InventoryReceiptUpdateRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
updated, err := c.receiptService.Update(ctx, req, id)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_UPDATE_INVENTORY_RECEIPT, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_UPDATE_INVENTORY_RECEIPT, updated)
ctx.JSON(http.StatusOK, res)
}
func (c *inventoryReceiptController) Delete(ctx *gin.Context) {
id := ctx.Param("id")
if err := c.receiptService.Delete(ctx, id); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_DELETE_INVENTORY_RECEIPT, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_DELETE_INVENTORY_RECEIPT, nil)
ctx.JSON(http.StatusOK, res)
}
func (c *inventoryReceiptController) GetById(ctx *gin.Context) {
id := ctx.Param("id")
receipt, err := c.receiptService.GetById(ctx, id)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_INVENTORY_RECEIPT, err.Error(), nil)
ctx.JSON(http.StatusNotFound, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_INVENTORY_RECEIPT, receipt)
ctx.JSON(http.StatusOK, res)
}
func (c *inventoryReceiptController) GetAll(ctx *gin.Context) {
clientId := ctx.DefaultQuery("client_id", "")
var filter query.InventoryReceiptFilter
filter.ClientID = clientId
if err := ctx.ShouldBindQuery(&filter); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_INVENTORY_RECEIPT, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
getAll := ctx.Query("get_all")
if getAll != "" {
receipts, _, err := c.receiptService.GetAll(ctx, filter)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_INVENTORY_RECEIPT, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
response := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_INVENTORY_RECEIPT, receipts)
ctx.JSON(http.StatusOK, response)
return
}
perPage := utils.ParseInt(ctx.DefaultQuery("per_page", "10"))
page := utils.ParseInt(ctx.DefaultQuery("page", "1"))
filter.PerPage = perPage
filter.Page = (page - 1) * perPage
receipts, total, err := c.receiptService.GetAll(ctx, filter)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_INVENTORY_RECEIPT, err.Error(), nil)
ctx.JSON(http.StatusInternalServerError, res)
return
}
paginationResponse := utils.BuildPaginationResponse(perPage, page, total)
res := utils.BuildResponseSuccessWithPagination(http.StatusOK, dto.MESSAGE_SUCCESS_GET_INVENTORY_RECEIPT, receipts, paginationResponse)
ctx.JSON(http.StatusOK, res)
}
func NewInventoryReceiptController(i *do.Injector, receiptService service.InventoryReceiptService) InventoryReceiptController {
db := do.MustInvokeNamed[*gorm.DB](i, constants.DB)
return &inventoryReceiptController{
receiptService: receiptService,
db: db,
}
}

View File

@ -0,0 +1,87 @@
package dto
import pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto"
const (
MESSAGE_FAILED_CREATE_INVENTORY_RECEIPT = "failed create inventory receipt"
MESSAGE_FAILED_CREATE_INVENTORY_RECEIPT_LINE = "failed create inventory receipt line"
MESSAGE_SUCCESS_CREATE_INVENTORY_RECEIPT = "success create inventory receipt"
MESSAGE_SUCCESS_CREATE_INVENTORY_RECEIPT_LINE = "success create inventory receipt line"
MESSAGE_FAILED_GET_INVENTORY_RECEIPT = "failed get inventory receipt"
MESSAGE_SUCCESS_GET_INVENTORY_RECEIPT = "success get inventory receipt"
MESSAGE_FAILED_UPDATE_INVENTORY_RECEIPT = "failed update inventory receipt"
MESSAGE_FAILED_UPDATE_INVENTORY_RECEIPT_LINE = "failed update inventory receipt line"
MESSAGE_SUCCESS_UPDATE_INVENTORY_RECEIPT = "success update inventory receipt"
MESSAGE_SUCCESS_UPDATE_INVENTORY_RECEIPT_LINE = "success update inventory receipt line"
MESSAGE_FAILED_DELETE_INVENTORY_RECEIPT = "failed delete inventory receipt"
MESSAGE_FAILED_DELETE_INVENTORY_RECEIPT_LINE = "failed delete inventory receipt line"
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"
)
type InventoryReceiptCreateRequest struct {
ReferenceNumber string `json:"reference_number"`
ReceiptDate string `json:"receipt_date"`
Source string `json:"source"`
QrCodeFile string `json:"qr_code_file"`
ClientID string `json:"client_id" binding:"required"`
ReceiptLines []InventoryReceiptLineCreateRequest `json:"inventory_lines,omitempty" binding:"dive"`
}
type InventoryReceiptLineCreateRequest struct {
Quantity float64 `json:"quantity"`
BatchNumber string `json:"batch_number"`
RepackingSuggestion string `json:"repacking_suggestion"`
RepackUomID string `json:"repack_uom_id"`
ProductID string `json:"product_id"`
ClientID string `json:"client_id"`
}
type InventoryReceiptUpdateRequest struct {
ReferenceNumber string `json:"reference_number"`
ReceiptDate string `json:"receipt_date"`
Source string `json:"source"`
QrCodeFile string `json:"qr_code_file"`
}
type InventoryReceiptResponse struct {
ID string `json:"id"`
ReferenceNumber string `json:"reference_number"`
DocumentNumber string `json:"document_number"`
ReceiptDate string `json:"receipt_date"`
Source string `json:"source"`
QrCodeFile string `json:"qr_code_file"`
Client pkgdto.IdNameResponse `json:"client"`
ReceiptLines []InventoryReceiptLineResponse `json:"inventory_lines"`
LineCount int `json:"line_count"`
}
type InventoryReceiptLineResponse struct {
ID string `json:"id"`
Quantity float64 `json:"quantity"`
BatchNumber string `json:"batch_number"`
RepackingSuggestion string `json:"repacking_suggestion"`
RepackUomID *string `json:"repack_uom_id"`
Product InventoryReceiptLineProductResponse `json:"product"`
ClientID string `json:"client_id"`
}
type InventoryReceiptLineProductResponse struct {
ID string `json:"id"`
Name string `json:"name"`
RefNumber string `json:"ref_number"`
Uom pkgdto.IdNameResponse `json:"uom"`
DimLength float64 `json:"dim_length"`
DimWidth float64 `json:"dim_width"`
DimHeight float64 `json:"dim_height"`
DimUom pkgdto.IdNameResponse `json:"dim_uom"`
}
type InventoryReceiptLineUpdateRequest struct {
Quantity *float64 `json:"quantity"`
BatchNumber *string `json:"batch_number"`
RepackingSuggestion *string `json:"repacking_suggestion"`
RepackUomID *string `json:"repack_uom_id"`
ProductID *string `json:"product_id"`
}

View File

@ -0,0 +1,22 @@
package query
import (
"gorm.io/gorm"
)
type InventoryReceiptFilter struct {
ClientID string `form:"client_id"`
Source string `form:"source"`
PerPage int `form:"per_page"`
Page int `form:"page"`
}
func ApplyInventoryReceiptFilters(db *gorm.DB, filter InventoryReceiptFilter) *gorm.DB {
if filter.ClientID != "" {
db = db.Where("client_id = ?", filter.ClientID)
}
if filter.Source != "" {
db = db.Where("source ILIKE ?", "%"+filter.Source+"%")
}
return db
}

View File

@ -0,0 +1,89 @@
package repository
import (
"context"
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
"gorm.io/gorm"
)
type InventoryReceiptLineRepository interface {
Create(ctx context.Context, tx *gorm.DB, line entities.TInventoryReceiptLineEntity) (entities.TInventoryReceiptLineEntity, error)
GetById(ctx context.Context, tx *gorm.DB, id string) (entities.TInventoryReceiptLineEntity, error)
GetAllByReceiptId(ctx context.Context, receiptId string) ([]entities.TInventoryReceiptLineEntity, error)
Update(ctx context.Context, tx *gorm.DB, line entities.TInventoryReceiptLineEntity) (entities.TInventoryReceiptLineEntity, error)
Delete(ctx context.Context, tx *gorm.DB, id string) error
BulkCreate(ctx context.Context, tx *gorm.DB, lines []entities.TInventoryReceiptLineEntity) error
DeleteByReceiptId(ctx context.Context, tx *gorm.DB, receiptId string) error
}
type inventoryReceiptLineRepository struct {
db *gorm.DB
}
func NewInventoryReceiptLineRepository(db *gorm.DB) InventoryReceiptLineRepository {
return &inventoryReceiptLineRepository{db: db}
}
func (r *inventoryReceiptLineRepository) Create(ctx context.Context, tx *gorm.DB, line entities.TInventoryReceiptLineEntity) (entities.TInventoryReceiptLineEntity, error) {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Create(&line).Error; err != nil {
return line, err
}
return line, nil
}
func (r *inventoryReceiptLineRepository) GetById(ctx context.Context, tx *gorm.DB, id string) (entities.TInventoryReceiptLineEntity, error) {
if tx == nil {
tx = r.db
}
var line entities.TInventoryReceiptLineEntity
if err := tx.WithContext(ctx).Preload("Product").Preload("RepackUom").Preload("InvReceipt").Preload("Client").First(&line, "id = ?", id).Error; err != nil {
return line, err
}
return line, nil
}
func (r *inventoryReceiptLineRepository) GetAllByReceiptId(ctx context.Context, receiptId string) ([]entities.TInventoryReceiptLineEntity, error) {
var lines []entities.TInventoryReceiptLineEntity
if err := r.db.WithContext(ctx).Where("inv_receipt_id = ?", receiptId).Preload("Product").Preload("RepackUom").Preload("InvReceipt").Preload("Client").Find(&lines).Error; err != nil {
return lines, err
}
return lines, nil
}
func (r *inventoryReceiptLineRepository) Update(ctx context.Context, tx *gorm.DB, line entities.TInventoryReceiptLineEntity) (entities.TInventoryReceiptLineEntity, error) {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Model(&entities.TInventoryReceiptLineEntity{}).Where("id = ?", line.ID).Select("*").Updates(&line).Error; err != nil {
return line, err
}
return line, nil
}
func (r *inventoryReceiptLineRepository) Delete(ctx context.Context, tx *gorm.DB, id string) error {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Delete(&entities.TInventoryReceiptLineEntity{}, "id = ?", id).Error; err != nil {
return err
}
return nil
}
func (r *inventoryReceiptLineRepository) BulkCreate(ctx context.Context, tx *gorm.DB, lines []entities.TInventoryReceiptLineEntity) error {
if tx == nil {
tx = r.db
}
return tx.WithContext(ctx).Create(&lines).Error
}
func (r *inventoryReceiptLineRepository) DeleteByReceiptId(ctx context.Context, tx *gorm.DB, receiptId string) error {
if tx == nil {
tx = r.db
}
return tx.WithContext(ctx).Where("inv_receipt_id = ?", receiptId).Delete(&entities.TInventoryReceiptLineEntity{}).Error
}

View File

@ -0,0 +1,86 @@
package repository
import (
"context"
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
"github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/query"
"gorm.io/gorm"
)
type InventoryReceiptRepository interface {
Create(ctx context.Context, tx *gorm.DB, receipt entities.TInventoryReceiptEntity) (entities.TInventoryReceiptEntity, error)
GetById(ctx context.Context, tx *gorm.DB, id string) (entities.TInventoryReceiptEntity, error)
GetAll(ctx context.Context, filter query.InventoryReceiptFilter) ([]entities.TInventoryReceiptEntity, int64, error)
Update(ctx context.Context, tx *gorm.DB, receipt entities.TInventoryReceiptEntity) (entities.TInventoryReceiptEntity, error)
Delete(ctx context.Context, tx *gorm.DB, id string) error
}
type inventoryReceiptRepository struct {
db *gorm.DB
}
func NewInventoryReceiptRepository(db *gorm.DB) InventoryReceiptRepository {
return &inventoryReceiptRepository{db: db}
}
func (r *inventoryReceiptRepository) Create(ctx context.Context, tx *gorm.DB, receipt entities.TInventoryReceiptEntity) (entities.TInventoryReceiptEntity, error) {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Create(&receipt).Error; err != nil {
return receipt, err
}
return receipt, nil
}
func (r *inventoryReceiptRepository) GetById(ctx context.Context, tx *gorm.DB, id string) (entities.TInventoryReceiptEntity, error) {
if tx == nil {
tx = r.db
}
var receipt entities.TInventoryReceiptEntity
if err := tx.WithContext(ctx).
Preload("Client").
Preload("ReceiptLines").
Preload("ReceiptLines.Product").
Preload("ReceiptLines.Product.Uom").
Preload("ReceiptLines.Product.DimUom").
First(&receipt, "id = ?", id).Error; err != nil {
return receipt, err
}
return receipt, nil
}
func (r *inventoryReceiptRepository) GetAll(ctx context.Context, filter query.InventoryReceiptFilter) ([]entities.TInventoryReceiptEntity, int64, error) {
var receipts []entities.TInventoryReceiptEntity
var total int64
db := query.ApplyInventoryReceiptFilters(r.db, filter)
db.Model(&entities.TInventoryReceiptEntity{}).Count(&total)
if filter.PerPage > 0 && filter.Page > 0 {
db = db.Offset((filter.Page - 1) * filter.PerPage).Limit(filter.PerPage)
}
if err := db.Preload("Client").Find(&receipts).Error; err != nil {
return receipts, total, err
}
return receipts, total, nil
}
func (r *inventoryReceiptRepository) Update(ctx context.Context, tx *gorm.DB, receipt entities.TInventoryReceiptEntity) (entities.TInventoryReceiptEntity, error) {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Model(&entities.TInventoryReceiptEntity{}).Where("id = ?", receipt.ID).Select("*").Updates(&receipt).Error; err != nil {
return receipt, err
}
return receipt, nil
}
func (r *inventoryReceiptRepository) Delete(ctx context.Context, tx *gorm.DB, id string) error {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Delete(&entities.TInventoryReceiptEntity{}, "id = ?", id).Error; err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,27 @@
package inventory_receipt
import (
"github.com/Caknoooo/go-gin-clean-starter/middlewares"
"github.com/Caknoooo/go-gin-clean-starter/modules/auth/service"
"github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/controller"
"github.com/Caknoooo/go-gin-clean-starter/pkg/constants"
"github.com/gin-gonic/gin"
"github.com/samber/do"
)
func RegisterRoutes(server *gin.Engine, injector *do.Injector) {
receiptController := do.MustInvoke[controller.InventoryReceiptController](injector)
jwtService := do.MustInvokeNamed[service.JWTService](injector, constants.JWTService)
receiptRoutes := server.Group("/api/v1/inventory-receipts")
{
receiptRoutes.POST("", middlewares.Authenticate(jwtService), receiptController.Create)
receiptRoutes.GET(":id", middlewares.Authenticate(jwtService), receiptController.GetById)
receiptRoutes.PUT(":id", middlewares.Authenticate(jwtService), receiptController.Update)
receiptRoutes.DELETE(":id", middlewares.Authenticate(jwtService), receiptController.Delete)
receiptRoutes.GET("", middlewares.Authenticate(jwtService), receiptController.GetAll)
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)
}
}

View File

@ -0,0 +1,391 @@
package service
import (
"context"
"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"
pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto"
"github.com/Caknoooo/go-gin-clean-starter/pkg/utils"
"github.com/google/uuid"
"gorm.io/gorm"
)
type InventoryReceiptService interface {
Create(ctx context.Context, req dtodomain.InventoryReceiptCreateRequest) (dtodomain.InventoryReceiptResponse, error)
GetById(ctx context.Context, id string) (dtodomain.InventoryReceiptResponse, error)
GetAll(ctx context.Context, filter query.InventoryReceiptFilter) ([]dtodomain.InventoryReceiptResponse, int64, error)
Update(ctx context.Context, req dtodomain.InventoryReceiptUpdateRequest, id string) (dtodomain.InventoryReceiptResponse, error)
Delete(ctx context.Context, id string) error
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
}
type inventoryReceiptService struct {
db *gorm.DB
receiptRepo repository.InventoryReceiptRepository
receiptLineRepo repository.InventoryReceiptLineRepository
}
// DeleteLine implements InventoryReceiptService.
func (s *inventoryReceiptService) DeleteLine(ctx context.Context, lineId string) error {
return s.receiptLineRepo.Delete(ctx, nil, lineId)
}
// UpdateLine implements InventoryReceiptService.
func (s *inventoryReceiptService) UpdateLine(ctx context.Context, lineId string, req dtodomain.InventoryReceiptLineUpdateRequest) (dtodomain.InventoryReceiptLineResponse, error) {
line, err := s.receiptLineRepo.GetById(ctx, nil, lineId)
if err != nil {
return dtodomain.InventoryReceiptLineResponse{}, err
}
if req.Quantity != nil {
line.Quantity = *req.Quantity
}
if req.BatchNumber != nil {
line.BatchNumber = *req.BatchNumber
}
if req.RepackingSuggestion != nil {
line.RepackingSuggestion = *req.RepackingSuggestion
}
if req.RepackUomID != nil {
if *req.RepackUomID != "" {
tmp, err := uuid.Parse(*req.RepackUomID)
if err != nil {
return dtodomain.InventoryReceiptLineResponse{}, err
}
line.RepackUomID = &tmp
} else {
line.RepackUomID = nil
}
}
if req.ProductID != nil {
if *req.ProductID != "" {
tmp, err := uuid.Parse(*req.ProductID)
if err != nil {
return dtodomain.InventoryReceiptLineResponse{}, err
}
line.ProductID = tmp
} else {
line.ProductID = uuid.Nil
}
}
updated, err := s.receiptLineRepo.Update(ctx, nil, line)
if err != nil {
return dtodomain.InventoryReceiptLineResponse{}, err
}
var repackUomID *string
if updated.RepackUomID != nil {
tmp := updated.RepackUomID.String()
repackUomID = &tmp
}
product := dtodomain.InventoryReceiptLineProductResponse{}
if updated.Product.ID != uuid.Nil {
product = dtodomain.InventoryReceiptLineProductResponse{
ID: updated.Product.ID.String(),
Name: updated.Product.Name,
}
}
return dtodomain.InventoryReceiptLineResponse{
ID: updated.ID.String(),
Quantity: updated.Quantity,
BatchNumber: updated.BatchNumber,
RepackingSuggestion: updated.RepackingSuggestion,
RepackUomID: repackUomID,
Product: product,
ClientID: updated.ClientID.String(),
}, nil
}
func toInventoryReceiptResponse(e entities.TInventoryReceiptEntity) dtodomain.InventoryReceiptResponse {
client := pkgdto.IdNameResponse{}
if e.Client.ID != uuid.Nil {
client = pkgdto.IdNameResponse{
ID: e.Client.ID.String(),
Name: e.Client.Name,
}
}
lines := make([]dtodomain.InventoryReceiptLineResponse, 0)
for _, line := range e.ReceiptLines {
product := dtodomain.InventoryReceiptLineProductResponse{}
if line.Product.ID != uuid.Nil {
product = dtodomain.InventoryReceiptLineProductResponse{
ID: line.Product.ID.String(),
Name: line.Product.Name,
RefNumber: line.Product.RefNumber,
Uom: pkgdto.IdNameResponse{
ID: line.Product.Uom.ID.String(),
Name: line.Product.Uom.Name,
},
DimLength: line.Product.DimLength,
DimWidth: line.Product.DimWidth,
DimHeight: line.Product.DimHeight,
DimUom: pkgdto.IdNameResponse{
ID: line.Product.DimUom.ID.String(),
Name: line.Product.DimUom.Name,
},
}
}
var repackUomID *string
if line.RepackUomID != nil {
tmp := line.RepackUomID.String()
repackUomID = &tmp
} else {
repackUomID = nil
}
lines = append(lines, dtodomain.InventoryReceiptLineResponse{
ID: line.ID.String(),
Quantity: line.Quantity,
BatchNumber: line.BatchNumber,
RepackingSuggestion: line.RepackingSuggestion,
RepackUomID: repackUomID,
Product: product,
ClientID: line.ClientID.String(),
})
}
return dtodomain.InventoryReceiptResponse{
ID: e.ID.String(),
ReferenceNumber: e.ReferenceNumber,
DocumentNumber: e.DocumentNumber,
ReceiptDate: utils.DateTimeToString(e.ReceiptDate),
Source: e.Source,
QrCodeFile: e.QrCodeFile,
// ClientID: e.ClientID.String(),
Client: client,
LineCount: len(lines),
ReceiptLines: lines,
}
}
func (s *inventoryReceiptService) Create(ctx context.Context, req dtodomain.InventoryReceiptCreateRequest) (dtodomain.InventoryReceiptResponse, error) {
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
clientUUID, err := uuid.Parse(req.ClientID)
if err != nil {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
docNum, err := entities.GenerateDocumentNumber(s.db, req.ClientID)
if err != nil {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
receipt := entities.TInventoryReceiptEntity{
ReferenceNumber: req.ReferenceNumber,
DocumentNumber: docNum,
ReceiptDate: utils.StringToDateTime(req.ReceiptDate),
Source: req.Source,
QrCodeFile: req.QrCodeFile,
ClientID: clientUUID,
}
created, err := s.receiptRepo.Create(ctx, tx, receipt)
if err != nil {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
// Bulk create lines
var lines []entities.TInventoryReceiptLineEntity
for _, lineReq := range req.ReceiptLines {
var productUUID uuid.UUID
if lineReq.ProductID != "" {
productUUID, err = uuid.Parse(lineReq.ProductID)
if err != nil {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
} else {
productUUID = uuid.Nil
}
var repackUomUUID *uuid.UUID
if lineReq.RepackUomID != "" {
tmp, err := uuid.Parse(lineReq.RepackUomID)
if err != nil {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
repackUomUUID = &tmp
} else {
repackUomUUID = nil
}
clientLineUUID, err := uuid.Parse(lineReq.ClientID)
if err != nil {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
lines = append(lines, entities.TInventoryReceiptLineEntity{
Quantity: lineReq.Quantity,
BatchNumber: lineReq.BatchNumber,
RepackingSuggestion: lineReq.RepackingSuggestion,
RepackUomID: repackUomUUID,
InvReceiptID: created.ID,
ProductID: productUUID,
ClientID: clientLineUUID,
})
}
if len(lines) > 0 {
err = s.receiptLineRepo.BulkCreate(ctx, tx, lines)
if err != nil {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
}
tx.Commit()
result, err := s.receiptRepo.GetById(ctx, nil, created.ID.String())
if err != nil {
return dtodomain.InventoryReceiptResponse{}, err
}
return toInventoryReceiptResponse(result), nil
}
func (s *inventoryReceiptService) GetById(ctx context.Context, id string) (dtodomain.InventoryReceiptResponse, error) {
receipt, err := s.receiptRepo.GetById(ctx, nil, id)
if err != nil {
return dtodomain.InventoryReceiptResponse{}, err
}
return toInventoryReceiptResponse(receipt), nil
}
func (s *inventoryReceiptService) GetAll(ctx context.Context, filter query.InventoryReceiptFilter) ([]dtodomain.InventoryReceiptResponse, int64, error) {
receipts, total, err := s.receiptRepo.GetAll(ctx, filter)
if err != nil {
return nil, 0, err
}
var responses []dtodomain.InventoryReceiptResponse
for _, e := range receipts {
responses = append(responses, toInventoryReceiptResponse(e))
}
if responses == nil {
responses = make([]dtodomain.InventoryReceiptResponse, 0)
}
return responses, total, nil
}
func (s *inventoryReceiptService) Update(ctx context.Context, req dtodomain.InventoryReceiptUpdateRequest, 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
}
if req.ReferenceNumber != "" {
receipt.ReferenceNumber = req.ReferenceNumber
}
receipt.ReceiptDate = utils.StringToDateTime(req.ReceiptDate)
receipt.Source = req.Source
receipt.QrCodeFile = req.QrCodeFile
updated, err := s.receiptRepo.Update(ctx, tx, receipt)
if err != nil {
tx.Rollback()
return dtodomain.InventoryReceiptResponse{}, err
}
tx.Commit()
result, err := s.receiptRepo.GetById(ctx, nil, updated.ID.String())
if err != nil {
return dtodomain.InventoryReceiptResponse{}, err
}
return toInventoryReceiptResponse(result), nil
}
func (s *inventoryReceiptService) Delete(ctx context.Context, id string) error {
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := s.receiptRepo.Delete(ctx, tx, id); err != nil {
tx.Rollback()
return err
}
tx.Commit()
return nil
}
func (s *inventoryReceiptService) CreateLine(ctx context.Context, receiptId string, req dtodomain.InventoryReceiptLineCreateRequest) (dtodomain.InventoryReceiptLineResponse, error) {
receiptUUID, err := uuid.Parse(receiptId)
if err != nil {
return dtodomain.InventoryReceiptLineResponse{}, err
}
var productUUID uuid.UUID
if req.ProductID != "" {
productUUID, err = uuid.Parse(req.ProductID)
if err != nil {
return dtodomain.InventoryReceiptLineResponse{}, err
}
} else {
productUUID = uuid.Nil
}
var repackUomUUID *uuid.UUID
if req.RepackUomID != "" {
tmp, err := uuid.Parse(req.RepackUomID)
if err != nil {
return dtodomain.InventoryReceiptLineResponse{}, err
}
repackUomUUID = &tmp
} else {
repackUomUUID = nil
}
clientLineUUID, err := uuid.Parse(req.ClientID)
if err != nil {
return dtodomain.InventoryReceiptLineResponse{}, err
}
line := entities.TInventoryReceiptLineEntity{
Quantity: req.Quantity,
BatchNumber: req.BatchNumber,
RepackingSuggestion: req.RepackingSuggestion,
RepackUomID: repackUomUUID,
InvReceiptID: receiptUUID,
ProductID: productUUID,
ClientID: clientLineUUID,
}
created, err := s.receiptLineRepo.Create(ctx, nil, line)
if err != nil {
return dtodomain.InventoryReceiptLineResponse{}, err
}
var repackUomID *string
if created.RepackUomID != nil {
tmp := created.RepackUomID.String()
repackUomID = &tmp
}
product := dtodomain.InventoryReceiptLineProductResponse{}
if created.Product.ID != uuid.Nil {
product = dtodomain.InventoryReceiptLineProductResponse{
ID: created.Product.ID.String(),
Name: created.Product.Name,
}
}
return dtodomain.InventoryReceiptLineResponse{
ID: created.ID.String(),
Quantity: created.Quantity,
BatchNumber: created.BatchNumber,
RepackingSuggestion: created.RepackingSuggestion,
RepackUomID: repackUomID,
Product: product,
ClientID: created.ClientID.String(),
}, nil
}
func NewInventoryReceiptService(db *gorm.DB, receiptRepo repository.InventoryReceiptRepository, receiptLineRepo repository.InventoryReceiptLineRepository) InventoryReceiptService {
return &inventoryReceiptService{
db: db,
receiptRepo: receiptRepo,
receiptLineRepo: receiptLineRepo,
}
}

View File

@ -2,6 +2,7 @@ package utils
import ( import (
"strconv" "strconv"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -48,3 +49,45 @@ func ParseUUID(s string) uuid.UUID {
} }
return id return id
} }
// StringToDate parses a string to time.Time with layout "2006-01-02"
func StringToDate(s string) time.Time {
if t, err := time.Parse("2006-01-02", s); err == nil {
now := time.Now()
// Gabungkan tanggal dari input dengan jam:menit:detik dari waktu sekarang
return time.Date(
t.Year(), t.Month(), t.Day(),
now.Hour(), now.Minute(), now.Second(), now.Nanosecond(),
now.Location(),
)
}
return time.Time{}
}
// StringToDateTime parses a string to time.Time with layout "2006-01-02T15:04:05Z07:00"
func StringToDateTime(s string) time.Time {
// Coba parse RFC3339 dulu
if t, err := time.Parse(time.RFC3339, s); err == nil {
return t
}
// Jika gagal, coba parse "2006-01-02"
if t, err := time.Parse("2006-01-02", s); err == nil {
now := time.Now()
// Gabungkan tanggal dari input dengan jam:menit:detik dari waktu sekarang
return time.Date(
t.Year(), t.Month(), t.Day(),
now.Hour(), now.Minute(), now.Second(), now.Nanosecond(),
now.Location(),
)
}
// Jika gagal semua, return zero time
return time.Time{}
}
func DateToString(t time.Time) string {
return t.Format("2006-01-02")
}
func DateTimeToString(t time.Time) string {
return t.Format(time.RFC3339)
}

View File

@ -58,6 +58,11 @@ import (
aisleRepo "github.com/Caknoooo/go-gin-clean-starter/modules/aisle/repository" aisleRepo "github.com/Caknoooo/go-gin-clean-starter/modules/aisle/repository"
aisleService "github.com/Caknoooo/go-gin-clean-starter/modules/aisle/service" aisleService "github.com/Caknoooo/go-gin-clean-starter/modules/aisle/service"
inventoryReceiptController "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/controller"
inventoryReceiptLineRepo "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/repository"
inventoryReceiptRepo "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/repository"
inventoryReceiptService "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/service"
"github.com/Caknoooo/go-gin-clean-starter/modules/user/controller" "github.com/Caknoooo/go-gin-clean-starter/modules/user/controller"
"github.com/Caknoooo/go-gin-clean-starter/modules/user/repository" "github.com/Caknoooo/go-gin-clean-starter/modules/user/repository"
userService "github.com/Caknoooo/go-gin-clean-starter/modules/user/service" userService "github.com/Caknoooo/go-gin-clean-starter/modules/user/service"
@ -107,6 +112,8 @@ func RegisterDependencies(injector *do.Injector) {
warehouseRepository := warehouseRepo.NewWarehouseRepository(db) warehouseRepository := warehouseRepo.NewWarehouseRepository(db)
zonaRepository := zonaRepo.NewZonaRepository(db) zonaRepository := zonaRepo.NewZonaRepository(db)
aisleRepository := aisleRepo.NewAisleRepository(db) aisleRepository := aisleRepo.NewAisleRepository(db)
inventoryReceiptRepository := inventoryReceiptRepo.NewInventoryReceiptRepository(db)
inventoryReceiptLineRepository := inventoryReceiptLineRepo.NewInventoryReceiptLineRepository(db)
// Service // Service
userServ := userService.NewUserService(userRepository, refreshTokenRepository, jwtService, db) userServ := userService.NewUserService(userRepository, refreshTokenRepository, jwtService, db)
@ -122,6 +129,7 @@ func RegisterDependencies(injector *do.Injector) {
warehouseServ := warehouseService.NewWarehouseService(warehouseRepository, db) warehouseServ := warehouseService.NewWarehouseService(warehouseRepository, db)
zonaServ := zonaService.NewZonaService(zonaRepository, db) zonaServ := zonaService.NewZonaService(zonaRepository, db)
aisleServ := aisleService.NewAisleService(aisleRepository, db) aisleServ := aisleService.NewAisleService(aisleRepository, db)
inventoryReceiptServ := inventoryReceiptService.NewInventoryReceiptService(db, inventoryReceiptRepository, inventoryReceiptLineRepository)
// Controller // Controller
do.Provide( do.Provide(
@ -199,4 +207,9 @@ func RegisterDependencies(injector *do.Injector) {
return aisleController.NewAisleController(aisleServ), nil return aisleController.NewAisleController(aisleServ), nil
}, },
) )
do.Provide(
injector, func(i *do.Injector) (inventoryReceiptController.InventoryReceiptController, error) {
return inventoryReceiptController.NewInventoryReceiptController(i, inventoryReceiptServ), nil
},
)
} }