From 687d02313bd926500961aaf27f099637190946fd Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Sun, 18 Jan 2026 19:46:09 +0700 Subject: [PATCH 1/8] feat[BE]: Update TransferRelationDTO and service search logic to include warehouse names --- .../inventory/transfers/dto/transfer.dto.go | 77 ++++--------------- .../transfers/services/transfer.service.go | 6 +- 2 files changed, 19 insertions(+), 64 deletions(-) diff --git a/internal/modules/inventory/transfers/dto/transfer.dto.go b/internal/modules/inventory/transfers/dto/transfer.dto.go index 8fa4d158..73b1a66c 100644 --- a/internal/modules/inventory/transfers/dto/transfer.dto.go +++ b/internal/modules/inventory/transfers/dto/transfer.dto.go @@ -4,20 +4,17 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) type TransferRelationDTO struct { - Id uint64 `json:"id"` - TransferReason string `json:"transfer_reason"` - TransferDate string `json:"transfer_date"` - SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"` - DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"` -} - -type WarehouseSimpleDTO struct { - Id uint `json:"id"` - Name string `json:"name"` + Id uint64 `json:"id"` + MovementNumber string `json:"movement_number"` + TransferReason string `json:"transfer_reason"` + TransferDate string `json:"transfer_date"` + SourceWarehouse *warehouseDTO.WarehouseRelationDTO `json:"source_warehouse,omitempty"` + DestinationWarehouse *warehouseDTO.WarehouseRelationDTO `json:"destination_warehouse,omitempty"` } type ProductSimpleDTO struct { @@ -25,16 +22,6 @@ type ProductSimpleDTO struct { Name string `json:"name"` } -type AreaDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type LocationDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - type SupplierSimpleDTO struct { Id uint `json:"id"` Name string `json:"name"` @@ -48,13 +35,6 @@ type DocumentDTO struct { Size float64 `json:"size"` } -type WarehouseDetailDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Location *LocationDTO `json:"location"` - Area *AreaDTO `json:"area"` -} - type TransferListDTO struct { TransferRelationDTO CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` @@ -97,16 +77,19 @@ type TransferDeliveryItemDTO struct { } func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO { - var sourceWarehouse *WarehouseDetailDTO + var sourceWarehouse *warehouseDTO.WarehouseRelationDTO if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 { - sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse) + mapped := warehouseDTO.ToWarehouseRelationDTO(*e.FromWarehouse) + sourceWarehouse = &mapped } - var destinationWarehouse *WarehouseDetailDTO + var destinationWarehouse *warehouseDTO.WarehouseRelationDTO if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 { - destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse) + mapped := warehouseDTO.ToWarehouseRelationDTO(*e.ToWarehouse) + destinationWarehouse = &mapped } return TransferRelationDTO{ Id: e.Id, + MovementNumber: e.MovementNumber, TransferReason: e.Reason, TransferDate: e.CreatedAt.Format("2006-01-02"), SourceWarehouse: sourceWarehouse, @@ -114,38 +97,6 @@ func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO { } } -func toAreaDTO(a *entity.Area) *AreaDTO { - if a == nil { - return nil - } - return &AreaDTO{ - Id: a.Id, - Name: a.Name, - } -} - -func toLocationDTO(l *entity.Location) *LocationDTO { - if l == nil { - return nil - } - return &LocationDTO{ - Id: l.Id, - Name: l.Name, - } -} - -func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO { - if w == nil { - return nil - } - return &WarehouseDetailDTO{ - Id: w.Id, - Name: w.Name, - Location: toLocationDTO(w.Location), - Area: toAreaDTO(&w.Area), - } -} - func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { var createdUser *userDTO.UserRelationDTO if e.CreatedUser != nil { diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index 3f12b444..35ddb4b1 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -99,7 +99,11 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) if params.Search != "" { - db = db.Where("movement_number ILIKE ?", "%"+strings.TrimSpace(params.Search)+"%") + searchTerm := "%" + strings.TrimSpace(params.Search) + "%" + db = db.Joins("LEFT JOIN warehouses AS from_warehouses ON from_warehouses.id = stock_transfers.from_warehouse_id"). + Joins("LEFT JOIN warehouses AS to_warehouses ON to_warehouses.id = stock_transfers.to_warehouse_id"). + Where("movement_number ILIKE ? OR from_warehouses.name ILIKE ? OR to_warehouses.name ILIKE ?", + searchTerm, searchTerm, searchTerm) } return db.Order("created_at DESC").Order("updated_at DESC") }) From bac36b4f00ff68d4111c791f6e7235cd974dd80f Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Sun, 18 Jan 2026 20:36:33 +0700 Subject: [PATCH 2/8] fix[BE]: Update error messages in TransferService to provide clearer context in Indonesian --- .../transfers/services/transfer.service.go | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index 35ddb4b1..482916cb 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -122,9 +122,9 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e }) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found") + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Transfer dengan ID %d tidak ditemukan", id)) } - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer") + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data transfer dengan ID %d", id)) } return transferPtr, nil @@ -140,12 +140,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques ) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID)) + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk dengan ID %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID)) } - return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal") + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengecek stok produk %d di gudang asal", product.ProductID)) } if sourcePW.Quantity < product.ProductQty { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID)) + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak mencukupi. Tersedia: %.2f, Diminta: %.2f", product.ProductID, sourcePW.Quantity, product.ProductQty)) } pwIDs = append(pwIDs, sourcePW.Id) } @@ -165,10 +165,10 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock") + return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock untuk gudang tujuan") } if projectFlockKandang.ClosedAt != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing") + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Project flock untuk gudang tujuan sudah ditutup (closing) pada %s", projectFlockKandang.ClosedAt.Format("2006-01-02"))) } actorID, err := m.ActorIDFromContext(c) @@ -196,16 +196,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID)) } - return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier") + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data supplier dengan ID %d", delivery.SupplierID)) } if supplier.Category != string(utils.SupplierCategoryBOP) { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID)) + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier '%s' (ID: %d) bukan kategori BOP. Kategori saat ini: %s", supplier.Name, delivery.SupplierID, supplier.Category)) } } movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context()) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number") + return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor movement transfer") } transferDate, _ := utils.ParseDateString(req.TransferDate) @@ -243,16 +243,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques ) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID)) + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID)) } - return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source") + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang asal", product.ProductID)) } destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID( c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID), ) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse destination") + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data product warehouse untuk produk %d di gudang tujuan", product.ProductID)) } if errors.Is(err, gorm.ErrRecordNotFound) { ctx := c.Context() @@ -267,7 +267,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques ProjectFlockKandangId: &projectFlockKandangID, } if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat product warehouse destination") + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal membuat product warehouse untuk produk %d di gudang tujuan", product.ProductID)) } } @@ -313,7 +313,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques for _, prod := range item.Products { detail, ok := detailMap[uint64(prod.ProductID)] if !ok { - return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID) + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan dalam daftar transfer untuk delivery #%d", prod.ProductID, i+1)) } deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{ StockTransferDeliveryId: delivery.Id, @@ -376,7 +376,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques Tx: tx, }) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err)) + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err)) } if err := tx.Model(&entity.StockTransferDetail{}). @@ -385,7 +385,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques "usage_qty": consumeResult.UsageQuantity, "pending_qty": consumeResult.PendingQuantity, }).Error; err != nil { - return fmt.Errorf("gagal update usage tracking: %w", err) + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking usage untuk produk %d", product.ProductID)) } note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber) @@ -398,7 +398,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques Tx: tx, }) if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok di gudang tujuan untuk produk %d: %v", product.ProductID, err)) + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok untuk produk %d di gudang tujuan. Error: %v", product.ProductID, err)) } if err := tx.Model(&entity.StockTransferDetail{}). @@ -406,7 +406,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques Updates(map[string]interface{}{ "total_qty": replenishResult.AddedQuantity, }).Error; err != nil { - return fmt.Errorf("gagal update total tracking: %w", err) + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengupdate tracking total untuk produk %d", product.ProductID)) } } @@ -440,7 +440,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques }) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err)) + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal memproses transfer. Error: %v", err)) } result, err := s.GetOne(c, uint(entityTransfer.Id)) @@ -450,8 +450,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques if len(expensePayloads) > 0 { if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil { - s.Log.Errorf("Failed to sync expense for transfer %d: %+v", entityTransfer.Id, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to sync expense: %v", err)) + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal sinkronisasi data expense untuk transfer %s. Silakan cek manual di module expense", entityTransfer.MovementNumber)) } } @@ -478,19 +477,19 @@ func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, wa if errors.Is(err, gorm.ErrRecordNotFound) { return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID)) } - return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang") + return 0, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal mengambil data gudang dengan ID %d", warehouseID)) } if warehouse.KandangId == nil || *warehouse.KandangId == 0 { - return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID)) + return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang '%s' (ID: %d) belum terhubung ke kandang", warehouse.Name, warehouseID)) } projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId)) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId)) + return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak ada project flock aktif untuk kandang %d", *warehouse.KandangId)) } - return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang") + return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock kandang yang aktif") } return uint(projectFlockKandang.Id), nil From af7aabdec8511d61a7e913744773be65e7f20b81 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Sun, 18 Jan 2026 20:50:52 +0700 Subject: [PATCH 3/8] fix[BE]: Adjust validation for MarketingQuery limit to remove max constraint --- internal/modules/repports/validations/repport.validation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index bddb5ab1..b178202f 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -17,7 +17,7 @@ type ExpenseQuery struct { type MarketingQuery struct { Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` Search string `query:"search" validate:"omitempty,max=100"` CustomerId int64 `query:"customer_id" validate:"omitempty"` ProductId int64 `query:"product_id" validate:"omitempty"` From fb193fc61f2aa1d29a4fb653dd86ae80e5ffad04 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Sun, 18 Jan 2026 21:26:31 +0700 Subject: [PATCH 4/8] fix[BE]: Update GetAllWithFilters to enhance search functionality and join conditions --- .../salesorder_delivery_product.repository.go | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index f14988b1..8c0bad3d 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -141,25 +141,37 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C Where("marketing_delivery_products.delivery_date IS NOT NULL") if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" || filters.MarketingType != "" { - db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id") + db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_delivery_products.product_warehouse_id") } if filters.ProductId > 0 || filters.Search != "" || filters.MarketingType != "" { db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id") } - if filters.WarehouseId > 0 { + if filters.WarehouseId > 0 || filters.Search != "" { db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id") } if filters.Search != "" { db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id") - } + db = db.Joins("LEFT JOIN users AS sales_users ON sales_users.id = marketings.sales_person_id") - if filters.Search != "" { searchPattern := "%" + filters.Search + "%" - db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?", - searchPattern, searchPattern, searchPattern, searchPattern) + db = db.Where(`( + marketing_delivery_products.vehicle_number ILIKE ? OR + customers.name ILIKE ? OR + warehouses.name ILIKE ? OR + products.name ILIKE ? OR + sales_users.name ILIKE ? OR + CONCAT( + marketings.so_number, + '-', + COALESCE(TO_CHAR(marketing_delivery_products.delivery_date, 'YYYYMMDD'), ''), + '-', + COALESCE(product_warehouses.warehouse_id::text, '') + ) ILIKE ? + )`, + searchPattern, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern) } if filters.CustomerId > 0 { From 378d633ea49967af3f447ffb1e61e001b8c82fc8 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 19 Jan 2026 09:27:37 +0700 Subject: [PATCH 5/8] feat[BE]: Enhance payment allocation logic to support FIFO consumption for sales transactions --- .../repports/services/repport.service.go | 78 ++++++++++++++++--- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 2e07e212..48f9e205 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -464,9 +464,13 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C if err != nil { return nil, 0, err } - result = append(result, item) + + if len(item.Rows) > 0 { + result = append(result, item) + } } + totalCustomers = int64(len(result)) return result, totalCustomers, nil } @@ -503,14 +507,8 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID row.Status = status if status == "LUNAS" { - if previousBalance >= tx.TotalPrice { - days := 0 - row.AgingDay = &days - } else if paymentDate != nil { + if paymentDate != nil { days := int(paymentDate.Sub(tx.TransDate).Hours() / 24) - if days < 0 { - days = 0 - } row.AgingDay = &days } else { days := 0 @@ -518,9 +516,6 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID } } else { days := int(time.Since(tx.TransDate).Hours() / 24) - if days < 0 { - days = 0 - } row.AgingDay = &days } } else if tx.TransactionType == "PAYMENT" { @@ -586,6 +581,67 @@ func (s *repportService) determineSalesStatusAndPaymentDate(transactions []reppo // 4. BELUM LUNAS: no payment at all if previousBalance >= currentSales.TotalPrice { + // Cari payment yang digunakan untuk melunasi sales ini dengan FIFO + // Track payment allocations that are consumed by previous sales + type paymentAllocation struct { + date time.Time + amount float64 + consumed float64 + } + allocations := []paymentAllocation{} + runningBalance := 0.0 + + // Process all transactions before current sales to build allocation map + for i := 0; i < currentIndex; i++ { + if transactions[i].TransactionType == "PAYMENT" { + allocations = append(allocations, paymentAllocation{ + date: transactions[i].TransDate, + amount: transactions[i].PaymentAmount, + consumed: 0, + }) + runningBalance += transactions[i].PaymentAmount + } else if transactions[i].TransactionType == "SALES" { + salesAmount := transactions[i].TotalPrice + remainingToConsume := salesAmount + + // Consume from oldest allocations first (FIFO) + for j := range allocations { + if remainingToConsume <= 0 { + break + } + available := allocations[j].amount - allocations[j].consumed + if available > 0 { + consume := available + if consume > remainingToConsume { + consume = remainingToConsume + } + allocations[j].consumed += consume + remainingToConsume -= consume + } + } + runningBalance -= salesAmount + } + } + + // Now find which allocation covers the current sales + amountNeeded := currentSales.TotalPrice + for _, alloc := range allocations { + available := alloc.amount - alloc.consumed + if available > 0 { + if amountNeeded <= available { + // This allocation fully covers the sales + return "LUNAS", &alloc.date + } else { + // This allocation partially covers, continue to next + amountNeeded -= available + } + } + } + + // If we get here, use the oldest allocation + if len(allocations) > 0 { + return "LUNAS", &allocations[0].date + } return "LUNAS", nil } From 8cd9627a511bfcc7f5d935d5cb149da28bd65f04 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 19 Jan 2026 14:34:08 +0700 Subject: [PATCH 6/8] feat[BE]: Add requested_qty field to LayingTransferSource and update related logic for transfer operations --- ...ed_qty_to_laying_transfer_sources.down.sql | 4 ++ ...sted_qty_to_laying_transfer_sources.up.sql | 9 +++ internal/entities/laying_transfer_source.go | 1 + internal/modules/inventory/transfers/route.go | 6 +- .../chickins/services/chickin.service.go | 4 +- .../project_flock_population_repository.go | 23 +++++++- .../dto/transfer_laying.dto.go | 12 +++- .../services/transfer_laying.service.go | 58 ++++++++++++++++--- .../validations/transfer_laying.validation.go | 12 ++-- 9 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.down.sql create mode 100644 internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.up.sql diff --git a/internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.down.sql b/internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.down.sql new file mode 100644 index 00000000..7dd06499 --- /dev/null +++ b/internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.down.sql @@ -0,0 +1,4 @@ +-- Rollback: Remove requested_qty column from laying_transfer_sources table + +ALTER TABLE laying_transfer_sources +DROP COLUMN IF EXISTS requested_qty; diff --git a/internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.up.sql b/internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.up.sql new file mode 100644 index 00000000..dc28ca74 --- /dev/null +++ b/internal/database/migrations/20260119055524_add_requested_qty_to_laying_transfer_sources.up.sql @@ -0,0 +1,9 @@ +-- Add requested_qty column to laying_transfer_sources table +-- This field stores the quantity requested by user during create/update +-- Separate from UsageQty (FIFO consumed) and PendingUsageQty (FIFO pending) + +ALTER TABLE laying_transfer_sources +ADD COLUMN requested_qty NUMERIC(15,3) DEFAULT 0 NOT NULL; + +-- Add comment for documentation +COMMENT ON COLUMN laying_transfer_sources.requested_qty IS 'Quantity requested by user during create/update'; diff --git a/internal/entities/laying_transfer_source.go b/internal/entities/laying_transfer_source.go index e0b85774..b284746d 100644 --- a/internal/entities/laying_transfer_source.go +++ b/internal/entities/laying_transfer_source.go @@ -11,6 +11,7 @@ type LayingTransferSource struct { LayingTransferId uint `gorm:"index;not null"` SourceProjectFlockKandangId uint `gorm:"not null"` ProductWarehouseId *uint `gorm:""` + RequestedQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // Quantity requested by user UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field Note string `gorm:"type:text"` diff --git a/internal/modules/inventory/transfers/route.go b/internal/modules/inventory/transfers/route.go index d24dbcb4..f754148c 100644 --- a/internal/modules/inventory/transfers/route.go +++ b/internal/modules/inventory/transfers/route.go @@ -15,8 +15,8 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ route := v1.Group("/transfers") route.Use(m.Auth(u)) - route.Get("/",m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll) - route.Post("/",m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne) - route.Get("/:id",m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne) + route.Get("/", m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll) + route.Post("/", m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne) + route.Get("/:id", m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne) } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 84e98f2d..b39dca78 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -200,9 +200,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti newChikins = append(newChikins, newChickin) - totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId) + totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), chickinReq.ProductWarehouseId) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for project_flock_kandang %d", req.ProjectFlockKandangId)) + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for product warehouse %d", chickinReq.ProductWarehouseId)) } availableQty := productWarehouse.Quantity - totalPopulationQty diff --git a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go index 022da6a3..36fe8cbc 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "math" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -16,6 +17,7 @@ type ProjectFlockPopulationRepository interface { GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error) GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) + GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error) CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error @@ -111,7 +113,7 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProductWarehouseID(c err := r.DB().WithContext(ctx). Model(&entity.ProjectFlockPopulation{}). Where("product_warehouse_id = ?", productWarehouseID). - Select("COALESCE(SUM(total_qty), 0)"). + Select("COALESCE(SUM(total_qty - total_used_qty), 0)"). Scan(&total).Error if err != nil { return 0, err @@ -135,3 +137,22 @@ func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKand } return total, nil } + +func (r *projectFlockPopulationRepositoryImpl) GetTotalChickInByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (int64, error) { + var total float64 + err := r.DB().WithContext(ctx). + Table("project_flock_populations"). + Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty"). + Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id"). + Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID). + Scan(&total).Error + if err != nil { + return 0, err + } + + if total < 0 { + total = 0 + } + + return int64(math.Round(total)), nil +} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index e81d6cc5..dfc5e5d9 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -162,9 +162,19 @@ func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouse } func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO { + // Tampilkan requested qty sebelum approve, consumed qty setelah approve + var displayQty float64 + if source.UsageQty > 0 { + // Sudah di-approve dan di-consume, tampilkan actual consumed quantity + displayQty = source.UsageQty + } else { + // Belum di-approve, tampilkan requested quantity + displayQty = source.RequestedQty + } + return LayingTransferSourceDTO{ SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang), - Qty: source.UsageQty, // Ambil dari UsageQty (FIFO consumed quantity) + Qty: displayQty, ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse), Note: source.Note, } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 9732ad75..3fe0b0b7 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -110,8 +110,32 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ offset := (params.Page - 1) * params.Limit transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - db = s.withRelations(db) + // Apply search and filters + if params.Search != "" { + searchPattern := "%" + params.Search + "%" + db = db.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id"). + Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id"). + Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?", + searchPattern, searchPattern, searchPattern, searchPattern) + } + + if params.TransferDate != "" { + db = db.Where("transfer_date::date = ?::date", params.TransferDate) + } + + if params.FlockSource > 0 { + db = db.Where("from_project_flock_id = ?", params.FlockSource) + } + + if params.FlockDestination > 0 { + db = db.Where("to_project_flock_id = ?", params.FlockDestination) + } + db = db.Order("created_at DESC") + + // Apply relations for eager loading + db = s.withRelations(db) + return db }) @@ -216,7 +240,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) for _, sourceDetail := range req.SourceKandangs { if sourceDetail.Quantity <= 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang sumber harus lebih dari 0") + continue } totalSourceQty += sourceDetail.Quantity @@ -247,11 +271,18 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) for _, targetDetail := range req.TargetKandangs { if targetDetail.Quantity <= 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Jumlah kandang tujuan harus lebih dari 0") + continue } totalTargetQty += targetDetail.Quantity } + if totalSourceQty == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang sumber dengan jumlah lebih dari 0") + } + if totalTargetQty == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Minimal harus ada 1 kandang tujuan dengan jumlah lebih dari 0") + } + if totalSourceQty != totalTargetQty { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Jumlah total sumber (%.0f) harus sama dengan jumlah total tujuan (%.0f)", totalSourceQty, totalTargetQty)) } @@ -279,11 +310,16 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) } for _, sourceDetail := range req.SourceKandangs { + if sourceDetail.Quantity == 0 { + continue + } + productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] source := entity.LayingTransferSource{ LayingTransferId: createBody.Id, SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, + RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user UsageQty: 0, PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval ProductWarehouseId: &productWarehouseId, @@ -295,6 +331,9 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) } for _, targetDetail := range req.TargetKandangs { + if targetDetail.Quantity == 0 { + continue + } targetprojectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) if err != nil { @@ -463,8 +502,9 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, source := entity.LayingTransferSource{ LayingTransferId: id, SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, + RequestedQty: sourceDetail.Quantity, // Quantity yang diminta user UsageQty: 0, - PendingUsageQty: sourceDetail.Quantity, + PendingUsageQty: 0, // Di-set 0, biarkan FIFO Consume yang handle saat Approval ProductWarehouseId: &productWarehouseId, } if err := sourceRepo.CreateOne(c.Context(), &source, nil); err != nil { @@ -700,7 +740,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID)) } - note := fmt.Sprintf("Transfer to Laying #%s - Target Kandang", transfer.TransferNumber) + note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber) replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ StockableKey: fifo.StockableKeyTransferToLayingIn, StockableID: target.Id, @@ -814,15 +854,15 @@ func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, project kandangAvailableQty := make(map[uint]float64) for _, kandang := range kandangs { - - totalQty, err := s.ProjectFlockPopulationRepo.GetTotalQtyByProjectFlockKandangID(ctx.Context(), kandang.Id) + // Gunakan fungsi repository yang sama dengan recording service + totalAvailable, err := s.ProjectFlockPopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), kandang.Id) if err != nil { - s.Log.Warnf("Failed to get total qty for kandang %d: %+v", kandang.Id, err) + s.Log.Warnf("Failed to get available qty for kandang %d: %+v", kandang.Id, err) kandangAvailableQty[kandang.Id] = 0 continue } - kandangAvailableQty[kandang.Id] = totalQty + kandangAvailableQty[kandang.Id] = totalAvailable } return pf, kandangAvailableQty, nil diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index 45a73e48..06d52316 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -2,12 +2,12 @@ package validation type SourceKandangDetail struct { ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"` - Quantity float64 `json:"quantity" validate:"required,gt=0"` + Quantity float64 `json:"quantity"` } type TargetKandangDetail struct { ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"` - Quantity float64 `json:"quantity" validate:"required,gt=0"` + Quantity float64 `json:"quantity"` } type Create struct { @@ -29,8 +29,12 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty"` + TransferDate string `query:"transfer_date" validate:"omitempty"` + FlockSource uint `query:"flock_source" validate:"omitempty,number"` + FlockDestination uint `query:"flock_destination" validate:"omitempty,number"` } type Approve struct { From 768961d7d6baf7d9ba48b1fbc6274fabb384c49c Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 19 Jan 2026 14:39:43 +0700 Subject: [PATCH 7/8] fix[BE]: Refactor GetAll method to improve query parameter handling and formatting --- .../controllers/transfer_laying.controller.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go index d2ab6d0a..13c39334 100644 --- a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -25,8 +25,12 @@ func NewTransferLayingController(transferLayingService service.TransferLayingSer func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + TransferDate: c.Query("transfer_date", ""), + FlockSource: uint(c.QueryInt("flock_source", 0)), + FlockDestination: uint(c.QueryInt("flock_destination", 0)), } if query.Page < 1 || query.Limit < 1 { @@ -179,7 +183,6 @@ func (u *TransferLayingController) Approval(c *fiber.Ctx) error { }) } - func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error { projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32) if err != nil { From b240478ed5e19504cb5663cec8c29f6ba5b6de7b Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 19 Jan 2026 17:44:10 +0700 Subject: [PATCH 8/8] feat[BE]: Add notes field to Update validation and update approval logic in expense services --- .../modules/expenses/services/expense.service.go | 16 ++++++++++++++-- .../expenses/validations/expense.validation.go | 1 + .../services/transfer_expense_bridge.go | 15 ++++++++++++--- .../services/transfer_laying.service.go | 1 - .../modules/purchases/services/expense_bridge.go | 15 ++++++++++++--- 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 3bf2db55..8b42fbdf 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -396,6 +396,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) updateBody["supplier_id"] = *req.SupplierID } + if req.Notes != nil { + updateBody["notes"] = *req.Notes + } + if req.LocationID != nil { locationID := uint(*req.LocationID) updateBody["location_id"] = locationID @@ -568,20 +572,28 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) if err != nil { return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context") } - if *latestApproval.Action != entity.ApprovalActionUpdated { + + if *latestApproval.Action != entity.ApprovalActionUpdated && latestApproval.StepNumber > uint16(utils.ExpenseStepPengajuan) { approvalAction := entity.ApprovalActionUpdated + previousStep := approvalutils.ApprovalStep(latestApproval.StepNumber) - 1 + + if previousStep < utils.ExpenseStepPengajuan { + previousStep = utils.ExpenseStepPengajuan + } + if _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowExpense, id, - utils.ExpenseStepPengajuan, + previousStep, &approvalAction, actorID, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval step") } + } if s.DocumentSvc != nil && len(req.Documents) > 0 { diff --git a/internal/modules/expenses/validations/expense.validation.go b/internal/modules/expenses/validations/expense.validation.go index 4501b87d..3fb9ccd5 100644 --- a/internal/modules/expenses/validations/expense.validation.go +++ b/internal/modules/expenses/validations/expense.validation.go @@ -31,6 +31,7 @@ type Update struct { Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"` SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"` LocationID *uint64 `form:"location_id" json:"location_id" validate:"omitempty,gt=0"` + Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"` Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"` } diff --git a/internal/modules/inventory/transfers/services/transfer_expense_bridge.go b/internal/modules/inventory/transfers/services/transfer_expense_bridge.go index c4f28354..7b69c22a 100644 --- a/internal/modules/inventory/transfers/services/transfer_expense_bridge.go +++ b/internal/modules/inventory/transfers/services/transfer_expense_bridge.go @@ -140,12 +140,21 @@ func (b *transferExpenseBridge) markExpensesUpdated(ctx context.Context, expense if actorID == 0 { actorID = 1 } - svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db)) - action := entity.ApprovalActionUpdated + approvalRepo := commonRepo.NewApprovalRepository(b.db) + svc := commonSvc.NewApprovalService(approvalRepo) + action := entity.ApprovalActionCreated + for id := range expenseIDs { - if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil { + latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } + + if latestApproval == nil { + if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil { + return err + } + } } return nil } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 3fe0b0b7..e64b9cc2 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -133,7 +133,6 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ db = db.Order("created_at DESC") - // Apply relations for eager loading db = s.withRelations(db) return db diff --git a/internal/modules/purchases/services/expense_bridge.go b/internal/modules/purchases/services/expense_bridge.go index 56097a90..1210b3a1 100644 --- a/internal/modules/purchases/services/expense_bridge.go +++ b/internal/modules/purchases/services/expense_bridge.go @@ -167,12 +167,21 @@ func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[ if actorID == 0 { actorID = 1 } - svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db)) - action := entity.ApprovalActionUpdated + approvalRepo := commonRepo.NewApprovalRepository(b.db) + svc := commonSvc.NewApprovalService(approvalRepo) + action := entity.ApprovalActionCreated + for id := range expenseIDs { - if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil { + latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } + + if latestApproval == nil { + if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil { + return err + } + } } return nil }