diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index 30d29b14..7ecc86d8 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -318,6 +318,7 @@ func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlo Joins("JOIN suppliers s ON s.id = e.supplier_id"). Where("pfk.project_flock_id = ?", projectFlockID). Where("e.category = ?", "BOP"). + Where("e.realization_date IS NOT NULL"). Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi))) if projectFlockKandangID != nil && *projectFlockKandangID != 0 { @@ -889,17 +890,58 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka } func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) { - rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeTransfer), true) + incomingQuery := r.withCtx(ctx). + Table("stock_transfer_details AS std"). + Select(` + std.product_id AS product_id, + p.name AS product_name, + f.name AS flag, + st.transfer_date::timestamp AS date, + COALESCE(st.movement_number, '') AS reference, + COALESCE(std.total_qty, 0) AS qty_in, + 0 AS qty_out, + COALESCE(p.product_price, 0) AS price + `). + Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id"). + Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Joins("JOIN products p ON p.id = std.product_id"). + Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). + Where("w.kandang_id = ?", kandangID). + Where("f.name IN ?", sapronakFlagsAll) + incoming, err := scanAndGroupDetails(incomingQuery) if err != nil { return nil, nil, err } - in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string { - if ref := strings.TrimSpace(row.MovementNumber); ref != "" { - return ref - } - return fmt.Sprintf("TRF-%d", row.ID) - }) - return in, out, nil + + outgoingQuery := r.withCtx(ctx). + Table("stock_allocations AS sa"). + Select(` + std.product_id AS product_id, + p.name AS product_name, + f.name AS flag, + st.transfer_date::timestamp AS date, + COALESCE(st.movement_number, '') AS reference, + 0 AS qty_in, + COALESCE(SUM(sa.qty), 0) AS qty_out, + COALESCE(p.product_price, 0) AS price + `). + Joins("JOIN stock_transfer_details std ON std.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyStockTransferOut.String()). + Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id"). + Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Joins("JOIN products p ON p.id = std.product_id"). + Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). + Where("sa.status = ?", entity.StockAllocationStatusActive). + Where("w.kandang_id = ?", kandangID). + Where("f.name IN ?", sapronakFlagsAll). + Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price") + outgoing, err := scanAndGroupDetails(outgoingQuery) + if err != nil { + return nil, nil, err + } + + return incoming, outgoing, nil } type ActualUsageCostRow struct { diff --git a/internal/modules/closings/services/sapronak.service.go b/internal/modules/closings/services/sapronak.service.go index b923db5d..fc354f46 100644 --- a/internal/modules/closings/services/sapronak.service.go +++ b/internal/modules/closings/services/sapronak.service.go @@ -3,7 +3,6 @@ package service import ( "context" "strings" - "time" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -112,7 +111,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val } // We no longer filter by date for closing sapronak report; pass nil pointers. - items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag) + items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag) if err != nil { s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report") @@ -126,8 +125,6 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val KandangName: pfk.Kandang.Name, Period: pfk.Period, Status: status, - StartDate: nil, - EndDate: nil, TotalIncomingValue: totalIncoming, TotalUsageValue: totalUsage, Items: items, @@ -318,7 +315,7 @@ func buildSapronakDetails( return result } -func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) { +func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) { // For sapronak closing report we intentionally ignore date range // and aggregate all historical transactions for the kandang/project. incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId) @@ -419,6 +416,22 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj return groupMap[flag] } + resolveFlagName := func(productID uint, details []dto.SapronakDetailDTO) (string, string) { + flag := "" + name := "" + if item, ok := itemMap[productID]; ok { + flag = item.Flag + name = item.ProductName + } + if flag == "" && len(details) > 0 { + flag = details[0].Flag + } + if name == "" && len(details) > 0 { + name = details[0].ProductName + } + return flag, name + } + for _, row := range incoming { if !matchesFlag(row.Flag) { continue @@ -554,19 +567,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj } for productID, details := range incomingDetails { - flag := "" - name := "" - if item, ok := itemMap[productID]; ok { - flag = item.Flag - name = item.ProductName - } + flag, name := resolveFlagName(productID, details) if !matchesFlag(flag) { continue } group := ensureGroup(flag) for _, d := range details { - d.Flag = flag - d.ProductName = name + if d.Flag == "" { + d.Flag = flag + } + if d.ProductName == "" { + d.ProductName = name + } group.Items = append(group.Items, d) group.TotalMasuk += d.QtyMasuk group.TotalNilai += d.Nilai @@ -575,19 +587,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj } for productID, details := range adjIncoming { - flag := "" - name := "" - if item, ok := itemMap[productID]; ok { - flag = item.Flag - name = item.ProductName - } + flag, name := resolveFlagName(productID, details) if !matchesFlag(flag) { continue } group := ensureGroup(flag) for _, d := range details { - d.Flag = flag - d.ProductName = name + if d.Flag == "" { + d.Flag = flag + } + if d.ProductName == "" { + d.ProductName = name + } group.Items = append(group.Items, d) group.TotalMasuk += d.QtyMasuk group.TotalNilai += d.Nilai @@ -596,19 +607,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj } for productID, details := range usageDetails { - flag := "" - name := "" - if item, ok := itemMap[productID]; ok { - flag = item.Flag - name = item.ProductName - } + flag, name := resolveFlagName(productID, details) if !matchesFlag(flag) { continue } group := ensureGroup(flag) for _, d := range details { - d.Flag = flag - d.ProductName = name + if d.Flag == "" { + d.Flag = flag + } + if d.ProductName == "" { + d.ProductName = name + } group.Items = append(group.Items, d) group.TotalKeluar += d.QtyKeluar group.SaldoAkhir -= d.QtyKeluar @@ -616,19 +626,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj } for productID, details := range adjOutgoing { - flag := "" - name := "" - if item, ok := itemMap[productID]; ok { - flag = item.Flag - name = item.ProductName - } + flag, name := resolveFlagName(productID, details) if !matchesFlag(flag) { continue } group := ensureGroup(flag) for _, d := range details { - d.Flag = flag - d.ProductName = name + if d.Flag == "" { + d.Flag = flag + } + if d.ProductName == "" { + d.ProductName = name + } group.Items = append(group.Items, d) group.TotalKeluar += d.QtyKeluar group.SaldoAkhir -= d.QtyKeluar @@ -636,19 +645,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj } for productID, details := range transIncoming { - flag := "" - name := "" - if item, ok := itemMap[productID]; ok { - flag = item.Flag - name = item.ProductName - } + flag, name := resolveFlagName(productID, details) if !matchesFlag(flag) { continue } group := ensureGroup(flag) for _, d := range details { - d.Flag = flag - d.ProductName = name + if d.Flag == "" { + d.Flag = flag + } + if d.ProductName == "" { + d.ProductName = name + } group.Items = append(group.Items, d) group.TotalMasuk += d.QtyMasuk group.TotalNilai += d.Nilai @@ -657,19 +665,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj } for productID, details := range transOutgoing { - flag := "" - name := "" - if item, ok := itemMap[productID]; ok { - flag = item.Flag - name = item.ProductName - } + flag, name := resolveFlagName(productID, details) if !matchesFlag(flag) { continue } group := ensureGroup(flag) for _, d := range details { - d.Flag = flag - d.ProductName = name + if d.Flag == "" { + d.Flag = flag + } + if d.ProductName == "" { + d.ProductName = name + } group.Items = append(group.Items, d) group.TotalKeluar += d.QtyKeluar group.SaldoAkhir -= d.QtyKeluar diff --git a/internal/modules/master/suppliers/controllers/supplier.controller.go b/internal/modules/master/suppliers/controllers/supplier.controller.go index c427316d..a373b680 100644 --- a/internal/modules/master/suppliers/controllers/supplier.controller.go +++ b/internal/modules/master/suppliers/controllers/supplier.controller.go @@ -28,6 +28,7 @@ func (u *SupplierController) GetAll(c *fiber.Ctx) error { Limit: c.QueryInt("limit", 10), Search: c.Query("search", ""), Category: c.Query("category", ""), + Flag: c.Query("flag", ""), } if query.Page < 1 || query.Limit < 1 { diff --git a/internal/modules/master/suppliers/services/supplier.service.go b/internal/modules/master/suppliers/services/supplier.service.go index c331647d..d211ed9d 100644 --- a/internal/modules/master/suppliers/services/supplier.service.go +++ b/internal/modules/master/suppliers/services/supplier.service.go @@ -47,6 +47,10 @@ func (s supplierService) withRelations(db *gorm.DB) *gorm.DB { Preload("NonstockSuppliers.Nonstock.Flags") } +func (s supplierService) withListRelations(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser") +} + func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Supplier, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -63,7 +67,7 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit offset := (params.Page - 1) * params.Limit suppliers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - db = s.withRelations(db) + db = s.withListRelations(db) if params.Search != "" { return db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -72,7 +76,23 @@ func (s supplierService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit db = db.Where("category ILIKE ?", "%"+params.Category+"%") } - return db.Order("created_at DESC").Order("updated_at DESC") + if params.Flag != "" { + flag := strings.ToUpper(params.Flag) + db = db.Where(` + EXISTS ( + SELECT 1 + FROM nonstock_suppliers nsup + JOIN nonstocks n ON n.id = nsup.nonstock_id + JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ? + WHERE nsup.supplier_id = suppliers.id + AND UPPER(f.name) = ? + )`, + entity.FlagableTypeNonstock, + flag, + ) + } + + return db.Order("suppliers.created_at DESC").Order("suppliers.updated_at DESC") }) if err != nil { diff --git a/internal/modules/master/suppliers/validations/supplier.validation.go b/internal/modules/master/suppliers/validations/supplier.validation.go index ec02cd8e..720e784e 100644 --- a/internal/modules/master/suppliers/validations/supplier.validation.go +++ b/internal/modules/master/suppliers/validations/supplier.validation.go @@ -32,7 +32,8 @@ type Update struct { type Query struct { Page int `query:"page" validate:"omitempty,number,min=1"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + Limit int `query:"limit" validate:"omitempty,number,min=1"` + Flag string `query:"flag" validate:"omitempty"` Search string `query:"search" validate:"omitempty,max=50"` Category string `query:"category" validate:"omitempty,max=50"` } diff --git a/internal/modules/purchases/services/expense_bridge.go b/internal/modules/purchases/services/expense_bridge.go index 7e5cbd91..56097a90 100644 --- a/internal/modules/purchases/services/expense_bridge.go +++ b/internal/modules/purchases/services/expense_bridge.go @@ -36,6 +36,7 @@ type ExpenseReceivingPayload struct { TransportPerItem *float64 ReceivedQty float64 ReceivedDate *time.Time + VehicleNumber *string } type groupedItem struct { @@ -182,6 +183,22 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ } ctx := c.Context() + filtered := make([]ExpenseReceivingPayload, 0, len(updates)) + for _, upd := range updates { + if upd.SupplierID == 0 { + continue + } + if upd.TransportPerItem == nil || *upd.TransportPerItem <= 0 { + continue + } + if upd.VehicleNumber == nil || strings.TrimSpace(*upd.VehicleNumber) == "" { + continue + } + filtered = append(filtered, upd) + } + if len(filtered) == 0 { + return nil + } // Load current links to decide whether to update in place or recreate. type itemLink struct { @@ -205,9 +222,9 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ itemLinks := make(map[uint]itemLink) updatedExpenses := make(map[uint64]struct{}) - if len(updates) > 0 { - ids := make([]uint, 0, len(updates)) - for _, upd := range updates { + if len(filtered) > 0 { + ids := make([]uint, 0, len(filtered)) + for _, upd := range filtered { if upd.PurchaseItemID != 0 { ids = append(ids, upd.PurchaseItemID) } @@ -252,7 +269,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ groups := make(map[string][]groupedItem) - for _, payload := range updates { + for _, payload := range filtered { if payload.ReceivedDate == nil { return fiber.NewError(fiber.StatusBadRequest, "received_date is required") } diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 35ca2f75..f6337c8a 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -702,6 +702,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation warehouseID uint supplierID uint transportPerItem *float64 + vehicleNumber *string overrideWarehouse bool receivedQty float64 } @@ -756,7 +757,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation } visitedItems[payload.PurchaseItemID] = struct{}{} - supplierID := purchase.SupplierId + var supplierID uint if payload.ExpeditionVendorID != nil && *payload.ExpeditionVendorID != 0 { supplierID = *payload.ExpeditionVendorID } @@ -770,6 +771,15 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation transportPerItem = &val } + var vehicleNumber *string + if payload.VehicleNumber != nil && strings.TrimSpace(*payload.VehicleNumber) != "" { + val := strings.TrimSpace(*payload.VehicleNumber) + vehicleNumber = &val + } else if item.VehicleNumber != nil && strings.TrimSpace(*item.VehicleNumber) != "" { + val := strings.TrimSpace(*item.VehicleNumber) + vehicleNumber = &val + } + prepared = append(prepared, preparedReceiving{ item: item, payload: payload, @@ -777,6 +787,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation warehouseID: warehouseID, supplierID: supplierID, transportPerItem: transportPerItem, + vehicleNumber: vehicleNumber, overrideWarehouse: overrideWarehouse, receivedQty: receivedQty, }) @@ -964,6 +975,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation TransportPerItem: prep.transportPerItem, ReceivedQty: prep.receivedQty, ReceivedDate: &date, + VehicleNumber: prep.vehicleNumber, } receivingPayloads = append(receivingPayloads, payload) }