From bd0f89c521546007861db17911dd8bc5b3c3de49 Mon Sep 17 00:00:00 2001 From: ragilap Date: Mon, 26 Jan 2026 16:47:28 +0700 Subject: [PATCH] [FIX/BE-US] fix closing count sapronak,expense notes purchase --- .../closings/dto/closingSapronak.dto.go | 9 +++- .../repositories/closing.repository.go | 48 +++++++++++++------ .../purchases/services/expense_bridge.go | 41 +++++++++++++--- 3 files changed, 75 insertions(+), 23 deletions(-) diff --git a/internal/modules/closings/dto/closingSapronak.dto.go b/internal/modules/closings/dto/closingSapronak.dto.go index 81fe7ebd..92d3b2ee 100644 --- a/internal/modules/closings/dto/closingSapronak.dto.go +++ b/internal/modules/closings/dto/closingSapronak.dto.go @@ -196,7 +196,11 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin } for idx, item := range group.Items { - productKey := strings.ToUpper(flagKey + "|" + item.ProductName + "|" + item.NoReferensi + "|" + formatDate(item.Tanggal)) + refKey := strings.TrimSpace(item.NoReferensi) + productKey := strings.ToUpper(flagKey + "|" + item.ProductName + "|" + refKey) + if refKey == "" { + productKey = strings.ToUpper(flagKey + "|" + item.ProductName + "|" + formatDate(item.Tanggal)) + } baseRow := SapronakCategoryRowDTO{ ID: idx + 1, Date: formatDate(item.Tanggal), @@ -212,6 +216,9 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin switch strings.ToLower(item.JenisTransaksi) { case "pembelian", "adjustment masuk", "mutasi masuk": row.QtyIn += item.QtyMasuk + if item.Tanggal != nil { + row.Date = formatDate(item.Tanggal) + } if row.UnitPrice == 0 { if item.QtyMasuk > 0 && item.Nilai > 0 { row.UnitPrice = item.Nilai / item.QtyMasuk diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index daff5d35..82e6f4a7 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -709,6 +709,23 @@ var ( sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet) ) +func (r *ClosingRepositoryImpl) joinSapronakProductFlag(db *gorm.DB, productAlias string) *gorm.DB { + subquery := r.DB(). + Table("flags"). + Select("DISTINCT ON (flagable_id) flagable_id, name"). + Where("flagable_type = ?", entity.FlagableTypeProduct). + Where("name IN ?", sapronakFlagsAll). + Order(fmt.Sprintf( + "flagable_id, CASE WHEN name = '%s' THEN 1 WHEN name = '%s' THEN 2 WHEN name = '%s' THEN 3 WHEN name = '%s' THEN 4 ELSE 5 END", + utils.FlagDOC, + utils.FlagPullet, + utils.FlagPakan, + utils.FlagOVK, + )) + + return db.Joins("JOIN (?) f ON f.flagable_id = "+productAlias+".id", subquery) +} + func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow { m := make(map[uint][]SapronakDetailRow) for _, row := range rows { @@ -745,11 +762,12 @@ func (r *ClosingRepositoryImpl) usageQuery( COALESCE(p.product_price, 0) AS default_price `) db = applyJoins(db, joins...) - return db. + db = db. Joins("JOIN product_warehouses pw ON "+pwJoinCond). Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where(where, args...) + db = r.joinSapronakProductFlag(db, "p") + return db } func (r *ClosingRepositoryImpl) fetchSapronakUsage( @@ -780,10 +798,10 @@ func (r *ClosingRepositoryImpl) detailQuery( db := r.withCtx(ctx). Table(table). Joins("JOIN product_warehouses pw ON "+pwJoinCond). - Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct) + Joins("JOIN products p ON p.id = pw.product_id") db = applyJoins(db, joins...) + db = r.joinSapronakProductFlag(db, "p") return db.Select(selectSQL).Where(where, args...) } @@ -907,7 +925,6 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C `). Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("LEFT JOIN recording_stocks rs ON rs.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyRecordingStock.String()). Joins("LEFT JOIN recordings r ON r.id = rs.recording_id"). Joins("LEFT JOIN project_chickins pc_used ON pc_used.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyProjectChickin.String()). @@ -930,7 +947,8 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C `, fifo.UsableKeyRecordingStock.String(), projectFlockKandangID, fifo.UsableKeyProjectChickin.String(), projectFlockKandangID, - ). + ) + query = r.joinSapronakProductFlag(query, "p"). Group(` pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, lt.transfer_date, sl.created_at, pc.chick_in_date, r.record_datetime, @@ -942,15 +960,15 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C } func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB { - return r.withCtx(ctx). + db := r.withCtx(ctx). Table("purchase_items AS pi"). Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL"). Joins("JOIN products p ON p.id = pi.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("JOIN warehouses w ON w.id = pi.warehouse_id"). Where("w.kandang_id = ?", kandangID). Where("f.name IN ?", sapronakFlagsAll). Where("pi.received_date IS NOT NULL") + return r.joinSapronakProductFlag(db, "p") } func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) { @@ -1021,10 +1039,10 @@ func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID ui `). Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("JOIN warehouses w ON w.id = pw.warehouse_id") db = applyJoins(db, joins...) + db = r.joinSapronakProductFlag(db, "p") if err := db. Where("sl.loggable_type = ?", logType). @@ -1093,10 +1111,10 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand 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("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll) + incomingQuery = r.joinSapronakProductFlag(incomingQuery, "p") incoming, err := scanAndGroupDetails(incomingQuery) if err != nil { return nil, nil, err @@ -1121,10 +1139,10 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where("w.kandang_id = ?", kandangID). Where("(w_source.kandang_id IS NULL OR w_source.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll) + incomingLayingQuery = r.joinSapronakProductFlag(incomingLayingQuery, "p") incomingLaying, err := scanAndGroupDetails(incomingLayingQuery) if err != nil { return nil, nil, err @@ -1152,12 +1170,12 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = std.dest_product_warehouse_id"). Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.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("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll). Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price") + outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p") outgoing, err := scanAndGroupDetails(outgoingQuery) if err != nil { return nil, nil, err @@ -1183,12 +1201,12 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand 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 = pw.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("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)"). Where("f.name IN ?", sapronakFlagsAll). Group("lts.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price") + outgoingLayingQuery = r.joinSapronakProductFlag(outgoingLayingQuery, "p") outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery) if err != nil { return nil, nil, err @@ -1218,12 +1236,12 @@ func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectF Joins("JOIN marketings m ON m.id = mp.marketing_id"). Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where("sa.status = ?", entity.StockAllocationStatusActive). Where("pw.project_flock_kandang_id = ?", projectFlockKandangID). Where("f.name IN ?", sapronakFlagsAll). Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price") + query = r.joinSapronakProductFlag(query, "p") sales, err := scanAndGroupDetails(query) if err != nil { return nil, err @@ -1245,7 +1263,6 @@ func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectF Joins("JOIN marketings m ON m.id = mp.marketing_id"). Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). - Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("LEFT JOIN stock_allocations sa ON sa.usable_id = mdp.id AND sa.usable_type = ? AND sa.status = ?", fifo.UsableKeyMarketingDelivery.String(), entity.StockAllocationStatusActive, @@ -1256,6 +1273,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectF Where("f.name IN ?", sapronakFlagsAll). Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price") + nonFifoQuery = r.joinSapronakProductFlag(nonFifoQuery, "p") nonFifoSales, err := scanAndGroupDetails(nonFifoQuery) if err != nil { return nil, err diff --git a/internal/modules/purchases/services/expense_bridge.go b/internal/modules/purchases/services/expense_bridge.go index 1210b3a1..d3bf2bbf 100644 --- a/internal/modules/purchases/services/expense_bridge.go +++ b/internal/modules/purchases/services/expense_bridge.go @@ -45,10 +45,7 @@ type groupedItem struct { projectFK *uint kandangID *uint totalPrice float64 -} - -func groupingKey(supplierID uint, date time.Time, warehouseID uint) string { - return fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(date), warehouseID) + poNumber string } type expenseBridge struct { @@ -222,6 +219,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ purchase, err := b.purchaseRepo.GetByID(ctx, purchaseID, func(db *gorm.DB) *gorm.DB { return db. Preload("Items"). + Preload("Items.Product"). Preload("Items.Warehouse"). Preload("Items.Warehouse.Kandang") }) @@ -309,7 +307,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ // If supplier/date unchanged, update nonstock in place. if oldSupplier == supplierID && oldDate.Equal(newDate) { - note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID) + note := purchaseItemDisplayNote(item, payload.PurchaseItemID, purchasePoNumber(purchase)) if err := b.db.WithContext(ctx). Model(&entity.ExpenseNonstock{}). Where("id = ?", link.ExpenseNonstockID). @@ -340,7 +338,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ if err != nil { return err } - note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID) + note := purchaseItemDisplayNote(item, payload.PurchaseItemID, purchasePoNumber(purchase)) if err := b.db.WithContext(ctx). Model(&entity.Expense{}). Where("id = ?", link.ExpenseID). @@ -392,6 +390,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ projectFK: projectFK, kandangID: kandangID, totalPrice: totalPrice, + poNumber: purchasePoNumber(purchase), } newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID) @@ -410,7 +409,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ createdNonstockID = noteMap[payload.PurchaseItemID] } - note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID) + note := purchaseItemDisplayNote(item, payload.PurchaseItemID, purchasePoNumber(purchase)) updateBody := map[string]interface{}{ "expense_id": expenseDetail.Id, "qty": payload.ReceivedQty, @@ -483,6 +482,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ projectFK: projectFK, kandangID: kandangID, totalPrice: totalPrice, + poNumber: purchasePoNumber(purchase), }) } @@ -679,6 +679,14 @@ func (b *expenseBridge) linkExpenseNonstocksToItems(ctx context.Context, detail Update("expense_nonstock_id", expenseNonstockID).Error; err != nil { return err } + + note := purchaseItemDisplayNote(gi.item, gi.payload.PurchaseItemID, gi.poNumber) + if err := b.db.WithContext(ctx). + Model(&entity.ExpenseNonstock{}). + Where("id = ?", expenseNonstockID). + Update("notes", note).Error; err != nil { + return err + } } return nil @@ -709,3 +717,22 @@ func mapExpenseNotes(detail *expenseDto.ExpenseDetailDTO) map[uint]uint64 { } return result } + +func purchaseItemDisplayNote(item *entity.PurchaseItem, itemID uint, poNumber string) string { + poLabel := "PO" + if strings.TrimSpace(poNumber) != "" { + poLabel = strings.TrimSpace(poNumber) + } + productName := fmt.Sprintf("Item %d", itemID) + if item != nil && item.Product != nil && strings.TrimSpace(item.Product.Name) != "" { + productName = item.Product.Name + } + return fmt.Sprintf("%s (%s)", poLabel, productName) +} + +func purchasePoNumber(purchase *entity.Purchase) string { + if purchase == nil || purchase.PoNumber == nil { + return "" + } + return *purchase.PoNumber +}