mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'dev/teguh' into 'development'
FIX[BE]: fixing BE See merge request mbugroup/lti-api!207
This commit is contained in:
+4
@@ -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;
|
||||
+9
@@ -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';
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
@@ -118,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
|
||||
@@ -136,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)
|
||||
}
|
||||
@@ -159,15 +163,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Hanya validasi closing jika ada project flock kandang (warehouse punya kandang_id)
|
||||
if destPfkID > 0 {
|
||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
|
||||
}
|
||||
if projectFlockKandang.ClosedAt != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
|
||||
}
|
||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock untuk gudang tujuan")
|
||||
}
|
||||
if projectFlockKandang.ClosedAt != nil {
|
||||
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)
|
||||
@@ -195,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)
|
||||
@@ -242,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()
|
||||
@@ -273,7 +274,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
ProjectFlockKandangId: pfkID,
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +320,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,
|
||||
@@ -382,7 +383,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{}).
|
||||
@@ -391,7 +392,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)
|
||||
@@ -404,7 +405,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{}).
|
||||
@@ -412,7 +413,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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,7 +447,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))
|
||||
@@ -456,8 +457,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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,20 +471,13 @@ func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID u
|
||||
return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads)
|
||||
}
|
||||
|
||||
func (s *transferService) notifyExpenseDetailsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error {
|
||||
if s.ExpenseBridge == nil || transferID == 0 || len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.ExpenseBridge.OnItemsDeleted(ctx, transferID, items)
|
||||
}
|
||||
|
||||
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
||||
if err != nil {
|
||||
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))
|
||||
}
|
||||
|
||||
// Jika warehouse tidak punya kandang_id, return 0 tanpa error
|
||||
@@ -495,9 +488,9 @@ func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, wa
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -148,18 +148,30 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
+22
-1
@@ -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
|
||||
}
|
||||
|
||||
+6
-3
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -110,8 +110,31 @@ 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")
|
||||
|
||||
db = s.withRelations(db)
|
||||
|
||||
return db
|
||||
})
|
||||
|
||||
@@ -216,7 +239,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 +270,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 +309,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 +330,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 +501,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 +739,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 +853,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
|
||||
|
||||
+8
-4
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user