diff --git a/go.mod b/go.mod index abb6d004..d0ffe677 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/jackc/pgconn v1.14.1 + github.com/jackc/pgx/v5 v5.5.5 github.com/redis/go-redis/v9 v9.14.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/viper v1.19.0 @@ -60,7 +61,6 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.2 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.5.5 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect diff --git a/go.sum b/go.sum index 73b36464..ab7d76b4 100644 --- a/go.sum +++ b/go.sum @@ -262,14 +262,10 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= -github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= -github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index e9148927..9e06e50c 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -44,7 +44,9 @@ const ( P_ReportExpenseGetAll = "lti.repport.expense.list" P_ReportDeliveryGetAll = "lti.repport.delivery.list" P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list" + P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list" P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list" + P_ReportProductionResultGetAll = "lti.repport.production_result.list" ) const ( @@ -134,18 +136,18 @@ const ( P_NonstocksUpdateOne = "lti.master.nonstocks.update" P_NonstocksDeleteOne = "lti.master.nonstocks.delete" - P_ProductCategoriesGetAll = "lti.master.Product_categories.list" - P_ProductCategoriesGetOne = "lti.master.Product_categories.detail" - P_ProductCategoriesCreateOne = "lti.master.Product_categories.create" - P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update" - P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete" - - P_ProductsGetAll = "lti.master.Products.list" - P_ProductsGetOne = "lti.master.Products.detail" - P_ProductsCreateOne = "lti.master.Products.create" - P_ProductsUpdateOne = "lti.master.Products.update" - P_ProductsDeleteOne = "lti.master.Products.delete" + P_ProductCategoriesGetAll = "lti.master.product_categories.list" + P_ProductCategoriesGetOne = "lti.master.product_categories.detail" + P_ProductCategoriesCreateOne = "lti.master.product_categories.create" + P_ProductCategoriesUpdateOne = "lti.master.product_categories.update" + P_ProductCategoriesDeleteOne = "lti.master.product_categories.delete" + P_ProductsGetAll = "lti.master.products.list" + P_ProductsGetOne = "lti.master.products.detail" + P_ProductsCreateOne = "lti.master.products.create" + P_ProductsUpdateOne = "lti.master.products.update" + P_ProductsDeleteOne = "lti.master.products.delete" + P_SuppliersGetAll = "lti.master.suppliers.list" P_SuppliersGetOne = "lti.master.suppliers.detail" P_SuppliersCreateOne = "lti.master.suppliers.create" @@ -207,15 +209,15 @@ const ( ) const ( - P_PurchaseGetAll = "lti.Purchase.list" - P_PurchaseGetOne = "lti.Purchase.detail" - P_PurchaseCreateOne = "lti.Purchase.create" - P_PurchaseUpdateOne = "lti.Purchase.update" - P_PurchaseDeleteOne = "lti.Purchase.delete" - P_PurchaseItemDeleteOne = "lti.Purchase.delete.item" - P_PurchaseReceive = "lti.Purchase.receive" - P_PurchaseApprovalStaff = "lti.Purchase.approve.staff" - P_PurchaseApprovalManager = "lti.Purchase.approve.manager" + P_PurchaseGetAll = "lti.purchase.list" + P_PurchaseGetOne = "lti.purchase.detail" + P_PurchaseCreateOne = "lti.purchase.create" + P_PurchaseUpdateOne = "lti.purchase.update" + P_PurchaseDeleteOne = "lti.purchase.delete" + P_PurchaseItemDeleteOne = "lti.purchase.delete.item" + P_PurchaseReceive = "lti.purchase.receive" + P_PurchaseApprovalStaff = "lti.purchase.approve.staff" + P_PurchaseApprovalManager = "lti.purchase.approve.manager" ) const ( diff --git a/internal/modules/master/uoms/route.go b/internal/modules/master/uoms/route.go index 8ffbcb62..ff5e2bd5 100644 --- a/internal/modules/master/uoms/route.go +++ b/internal/modules/master/uoms/route.go @@ -15,15 +15,9 @@ func UomRoutes(v1 fiber.Router, u user.UserService, s uom.UomService) { route := v1.Group("/uoms") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) - - route.Get("/",m.RequirePermissions(m.P_AreaGetAll), ctrl.GetAll) - route.Post("/",m.RequirePermissions(m.P_AreaCreateOne), ctrl.CreateOne) - route.Get("/:id",m.RequirePermissions(m.P_AreaGetOne), ctrl.GetOne) - route.Patch("/:id",m.RequirePermissions(m.P_AreaUpdateOne), ctrl.UpdateOne) - route.Delete("/:id",m.RequirePermissions(m.P_AreaDeleteOne), ctrl.DeleteOne) + route.Get("/", m.RequirePermissions(m.P_UomsGetAll), ctrl.GetAll) + route.Post("/", m.RequirePermissions(m.P_UomsCreateOne), ctrl.CreateOne) + route.Get("/:id", m.RequirePermissions(m.P_UomsGetOne), ctrl.GetOne) + route.Patch("/:id", m.RequirePermissions(m.P_UomsUpdateOne), ctrl.UpdateOne) + route.Delete("/:id", m.RequirePermissions(m.P_UomsDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/purchases/route.go b/internal/modules/purchases/route.go index 0fe038c3..ed0c74f1 100644 --- a/internal/modules/purchases/route.go +++ b/internal/modules/purchases/route.go @@ -15,12 +15,12 @@ func Routes(router fiber.Router, purchaseService service.PurchaseService, userSe route := router.Group("/purchases") route.Use(m.Auth(userService)) - route.Get("/",m.RequirePermissions(m.P_PurchaseGetAll), ctrl.GetAll) - route.Get("/:id",m.RequirePermissions(m.P_PurchaseGetOne), ctrl.GetOne) - route.Post("/", ctrl.CreateOne) - route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase) - route.Post("/:id/approvals/manager", ctrl.ApproveManagerPurchase) - route.Post("/:id/receipts",ctrl.ReceiveProducts) - route.Delete("/:id", ctrl.DeletePurchase) - route.Delete("/:id/items", ctrl.DeleteItems) + route.Get("/", m.RequirePermissions(m.P_PurchaseGetAll), ctrl.GetAll) + route.Get("/:id", m.RequirePermissions(m.P_PurchaseGetOne), ctrl.GetOne) + route.Post("/", m.RequirePermissions(m.P_PurchaseCreateOne), ctrl.CreateOne) + route.Post("/:id/approvals/staff", m.RequirePermissions(m.P_PurchaseApprovalStaff), ctrl.ApproveStaffPurchase) + route.Post("/:id/approvals/manager", m.RequirePermissions(m.P_PurchaseApprovalManager), ctrl.ApproveManagerPurchase) + route.Post("/:id/receipts", m.RequirePermissions(m.P_PurchaseReceive), ctrl.ReceiveProducts) + route.Delete("/:id", m.RequirePermissions(m.P_PurchaseDeleteOne), ctrl.DeletePurchase) + route.Delete("/:id/items", m.RequirePermissions(m.P_PurchaseItemDeleteOne), ctrl.DeleteItems) } diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 39136e85..22ff4acf 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -3,6 +3,7 @@ package controller import ( "math" "strconv" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" @@ -164,6 +165,59 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { }) } +func (c *RepportController) GetDebtSupplier(ctx *fiber.Ctx) error { + supplierIDs, err := parseCommaSeparatedInt64s(ctx.Query("supplier_ids", "")) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + query := &validation.DebtSupplierQuery{ + Page: ctx.QueryInt("page", 1), + Limit: ctx.QueryInt("limit", 10), + SupplierIDs: supplierIDs, + StartDate: ctx.Query("start_date", ""), + EndDate: ctx.Query("end_date", ""), + FilterBy: ctx.Query("filter_by", ""), + SortOrder: ctx.Query("sort_order", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := c.RepportService.GetDebtSupplier(ctx, query) + if err != nil { + return err + } + + supplierIDs = query.SupplierIDs + if supplierIDs == nil { + supplierIDs = []int64{} + } + + filters := map[string]interface{}{ + "start_date": query.StartDate, + "end_date": query.EndDate, + "supplier_ids": supplierIDs, + "filter_by": query.FilterBy, + } + + return ctx.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.DebtSupplierDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get supplier debt recap successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + Filters: filters, + }, + Data: result, + }) +} + func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error { data, meta, err := c.RepportService.GetHppPerKandang(ctx) if err != nil { @@ -227,3 +281,27 @@ func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error { Data: data, }) } + +func parseCommaSeparatedInt64s(raw string) ([]int64, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return []int64{}, nil + } + + parts := strings.Split(raw, ",") + result := make([]int64, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + + id, err := strconv.ParseInt(part, 10, 64) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "supplier_ids must be comma separated integers") + } + result = append(result, id) + } + + return result, nil +} diff --git a/internal/modules/repports/dto/repportDebtSupplier.dto.go b/internal/modules/repports/dto/repportDebtSupplier.dto.go new file mode 100644 index 00000000..5dce055f --- /dev/null +++ b/internal/modules/repports/dto/repportDebtSupplier.dto.go @@ -0,0 +1,37 @@ +package dto + +import ( + areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" + supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" +) + +type DebtSupplierRowDTO struct { + PrNumber string `json:"pr_number"` + PoNumber string `json:"po_number"` + PrDate string `json:"pr_date"` + PoDate string `json:"po_date"` + Aging int `json:"aging"` + Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` + Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` + DueDate string `json:"due_date"` + DueStatus string `json:"due_status"` + TotalPrice float64 `json:"total_price"` + PaymentPrice float64 `json:"payment_price"` + DebtPrice float64 `json:"debt_price"` + Status string `json:"status"` + TravelNumber string `json:"travel_number"` +} + +type DebtSupplierTotalDTO struct { + Aging int `json:"aging"` + TotalPrice float64 `json:"total_price"` + PaymentPrice float64 `json:"payment_price"` + DebtPrice float64 `json:"debt_price"` +} + +type DebtSupplierDTO struct { + Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` + Rows []DebtSupplierRowDTO `json:"rows"` + Total DebtSupplierTotalDTO `json:"total"` +} diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index 40a3c0f3..61f37d4d 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -31,12 +31,13 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * recordingRepository := recordingRepo.NewRecordingRepository(db) approvalRepository := commonRepo.NewApprovalRepository(db) purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db) + debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db) hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db) productionResultRepository := repportRepo.NewProductionResultRepository(db) userRepository := rUser.NewUserRepository(db) approvalSvc := approvalService.NewApprovalService(approvalRepository) - repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, hppPerKandangRepository, productionResultRepository) + repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository) userService := sUser.NewUserService(userRepository, validate) RepportRoutes(router, userService, repportService) diff --git a/internal/modules/repports/repositories/debt_supplier.repository.go b/internal/modules/repports/repositories/debt_supplier.repository.go new file mode 100644 index 00000000..84e9402d --- /dev/null +++ b/internal/modules/repports/repositories/debt_supplier.repository.go @@ -0,0 +1,221 @@ +package repositories + +import ( + "context" + "fmt" + "strings" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "gorm.io/gorm" +) + +type DebtSupplierRepository interface { + GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error) + GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error) + GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error) +} + +type debtSupplierRepositoryImpl struct { + db *gorm.DB +} + +func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository { + return &debtSupplierRepositoryImpl{db: db} +} + +func resolveDebtSupplierDateColumn(filterBy string) string { + switch strings.ToLower(strings.TrimSpace(filterBy)) { + case "po_date": + return "purchases.po_date" + case "pr_date": + return "purchases.created_at" + case "do_date", "received_date", "": + return "purchase_items.received_date" + default: + return "purchase_items.received_date" + } +} + +func (r *debtSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.DebtSupplierQuery) *gorm.DB { + dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy) + + db := r.db.WithContext(ctx). + Model(&entity.Supplier{}). + Joins("JOIN purchases ON purchases.supplier_id = suppliers.id"). + Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id") + + if len(filters.SupplierIDs) > 0 { + db = db.Where("suppliers.id IN ?", filters.SupplierIDs) + } + + if filters.StartDate != "" { + if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) + } + } + + if filters.EndDate != "" { + if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo) + } + } + + return db +} + +func (r *debtSupplierRepositoryImpl) GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error) { + query := r.baseSupplierQuery(ctx, filters) + + var totalSuppliers int64 + if err := query. + Distinct("suppliers.id"). + Count(&totalSuppliers).Error; err != nil { + return nil, 0, err + } + + if totalSuppliers == 0 { + return []entity.Supplier{}, 0, nil + } + + if offset < 0 { + offset = 0 + } + + var supplierIDs []uint + if err := query. + Select("suppliers.id"). + Order("suppliers.id ASC"). + Offset(offset). + Limit(limit). + Pluck("suppliers.id", &supplierIDs).Error; err != nil { + return nil, 0, err + } + + if len(supplierIDs) == 0 { + return []entity.Supplier{}, totalSuppliers, nil + } + + var suppliers []entity.Supplier + if err := r.db.WithContext(ctx). + Where("id IN ?", supplierIDs). + Find(&suppliers).Error; err != nil { + return nil, 0, err + } + + return suppliers, totalSuppliers, nil +} + +func (r *debtSupplierRepositoryImpl) GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error) { + if len(supplierIDs) == 0 { + return []entity.Purchase{}, nil + } + + purchaseIDs, err := r.getPurchaseIDs(ctx, supplierIDs, filters) + if err != nil { + return nil, err + } + if len(purchaseIDs) == 0 { + return []entity.Purchase{}, nil + } + + preloadItems := func(db *gorm.DB) *gorm.DB { + db = db. + Preload("Warehouse"). + Preload("Warehouse.Area"). + Order("purchase_items.id ASC") + + if strings.EqualFold(strings.TrimSpace(filters.FilterBy), "do_date") || strings.EqualFold(strings.TrimSpace(filters.FilterBy), "received_date") || strings.TrimSpace(filters.FilterBy) == "" { + if filters.StartDate != "" { + if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where("DATE(purchase_items.received_date) >= ?", dateFrom) + } + } + if filters.EndDate != "" { + if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil { + db = db.Where("DATE(purchase_items.received_date) <= ?", dateTo) + } + } + } + + return db + } + + var purchases []entity.Purchase + if err := r.db.WithContext(ctx). + Model(&entity.Purchase{}). + Preload("Supplier"). + Preload("Items", preloadItems). + Where("purchases.id IN ?", purchaseIDs). + Order("purchases.id ASC"). + Find(&purchases).Error; err != nil { + return nil, err + } + + return purchases, nil +} + +func (r *debtSupplierRepositoryImpl) getPurchaseIDs(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]uint, error) { + dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy) + + db := r.db.WithContext(ctx). + Table("purchases"). + Select("DISTINCT purchases.id"). + Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). + Where("purchases.supplier_id IN ?", supplierIDs) + + if filters.StartDate != "" { + if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) + } + } + + if filters.EndDate != "" { + if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo) + } + } + + var purchaseIDs []uint + if err := db.Order("purchases.id ASC").Pluck("purchases.id", &purchaseIDs).Error; err != nil { + return nil, err + } + + return purchaseIDs, nil +} + +func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error) { + if len(supplierIDs) == 0 || len(references) == 0 { + return map[string]float64{}, nil + } + + type paymentRow struct { + ReferenceNumber *string `gorm:"column:reference_number"` + Total float64 `gorm:"column:total"` + } + + rows := make([]paymentRow, 0) + if err := r.db.WithContext(ctx). + Model(&entity.Payment{}). + Select("reference_number, SUM(nominal) AS total"). + Where("party_type = ?", string(utils.PaymentPartySupplier)). + Where("direction = ?", "OUT"). + Where("party_id IN ?", supplierIDs). + Where("reference_number IN ?", references). + Group("reference_number"). + Scan(&rows).Error; err != nil { + return nil, err + } + + result := make(map[string]float64, len(rows)) + for _, row := range rows { + if row.ReferenceNumber == nil || strings.TrimSpace(*row.ReferenceNumber) == "" { + continue + } + result[*row.ReferenceNumber] = row.Total + } + + return result, nil +} diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 0da9adb2..0a0cf8a3 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -18,7 +18,8 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense) route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing) route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier) - route.Get("/hpp-per-kandang", ctrl.GetHppPerKandang) - route.Get("/production-result/:idProjectFlockKandang", ctrl.GetProductionResult) + route.Get("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier) + route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppPerKandang) + route.Get("/production-result/:idProjectFlockKandang", m.RequirePermissions(m.P_ReportProductionResultGetAll), ctrl.GetProductionResult) } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index ebf68867..c7576e5f 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -18,6 +18,9 @@ import ( approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" + supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" @@ -34,6 +37,7 @@ type RepportService interface { GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) + GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) } @@ -48,6 +52,7 @@ type repportService struct { RecordingRepo recordingRepo.RecordingRepository ApprovalSvc approvalService.ApprovalService PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository + DebtSupplierRepo repportRepo.DebtSupplierRepository HppPerKandangRepo repportRepo.HppPerKandangRepository ProductionResultRepo repportRepo.ProductionResultRepository } @@ -70,6 +75,7 @@ func NewRepportService( recordingRepo recordingRepo.RecordingRepository, approvalSvc approvalService.ApprovalService, purchaseSupplierRepo repportRepo.PurchaseSupplierRepository, + debtSupplierRepo repportRepo.DebtSupplierRepository, hppPerKandangRepo repportRepo.HppPerKandangRepository, productionResultRepo repportRepo.ProductionResultRepository, ) RepportService { @@ -83,6 +89,7 @@ func NewRepportService( RecordingRepo: recordingRepo, ApprovalSvc: approvalSvc, PurchaseSupplierRepo: purchaseSupplierRepo, + DebtSupplierRepo: debtSupplierRepo, HppPerKandangRepo: hppPerKandangRepo, ProductionResultRepo: productionResultRepo, } @@ -634,6 +641,200 @@ func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.Pu return result, totalSuppliers, nil } +func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) { + if params.FilterBy == "" { + params.FilterBy = "do_date" + } + + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + if offset < 0 { + offset = 0 + } + + suppliers, totalSuppliers, err := s.DebtSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params) + if err != nil { + return nil, 0, err + } + if totalSuppliers == 0 || len(suppliers) == 0 { + return []dto.DebtSupplierDTO{}, totalSuppliers, nil + } + + supplierMap := make(map[uint]entity.Supplier, len(suppliers)) + supplierIDs := make([]uint, 0, len(suppliers)) + for _, supplier := range suppliers { + supplierMap[supplier.Id] = supplier + supplierIDs = append(supplierIDs, supplier.Id) + } + + purchases, err := s.DebtSupplierRepo.GetPurchasesBySuppliers(c.Context(), supplierIDs, params) + if err != nil { + return nil, 0, err + } + + purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs)) + references := make([]string, 0) + seenRefs := make(map[string]struct{}) + for _, purchase := range purchases { + supplierID := purchase.SupplierId + purchasesBySupplier[supplierID] = append(purchasesBySupplier[supplierID], purchase) + + reference := purchase.PrNumber + if purchase.PoNumber != nil && strings.TrimSpace(*purchase.PoNumber) != "" { + reference = *purchase.PoNumber + } + if _, exists := seenRefs[reference]; !exists { + seenRefs[reference] = struct{}{} + references = append(references, reference) + } + } + + paymentTotals, err := s.DebtSupplierRepo.GetPaymentTotalsByReferences(c.Context(), supplierIDs, references) + if err != nil { + return nil, 0, err + } + + location, err := time.LoadLocation("Asia/Jakarta") + if err != nil { + return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration") + } + now := time.Now().In(location) + + result := make([]dto.DebtSupplierDTO, 0, len(supplierIDs)) + for _, supplierID := range supplierIDs { + supplier, exists := supplierMap[supplierID] + if !exists { + continue + } + + items := purchasesBySupplier[supplierID] + rows := make([]dto.DebtSupplierRowDTO, 0, len(items)) + total := dto.DebtSupplierTotalDTO{} + + for _, purchase := range items { + row := buildDebtSupplierRow(purchase, paymentTotals, now, location) + rows = append(rows, row) + + if row.Aging > total.Aging { + total.Aging = row.Aging + } + total.TotalPrice += row.TotalPrice + total.PaymentPrice += row.PaymentPrice + total.DebtPrice += row.DebtPrice + } + + sortDesc := strings.EqualFold(params.SortOrder, "desc") + sort.SliceStable(rows, func(i, j int) bool { + if sortDesc { + return rows[i].PrDate > rows[j].PrDate + } + return rows[i].PrDate < rows[j].PrDate + }) + + var supplierDTORef *supplierDTO.SupplierRelationDTO + if supplier.Id != 0 { + mapped := supplierDTO.ToSupplierRelationDTO(supplier) + supplierDTORef = &mapped + } + + result = append(result, dto.DebtSupplierDTO{ + Supplier: supplierDTORef, + Rows: rows, + Total: total, + }) + } + + return result, totalSuppliers, nil +} + +func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]float64, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO { + prNumber := purchase.PrNumber + poNumber := "" + if purchase.PoNumber != nil { + poNumber = *purchase.PoNumber + } + + reference := prNumber + if strings.TrimSpace(poNumber) != "" { + reference = poNumber + } + + prDate := purchase.CreatedAt.In(loc) + startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc) + endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) + aging := int(endDate.Sub(startDate).Hours() / 24) + + totalPrice := 0.0 + travelNumber := "-" + var area *areaDTO.AreaRelationDTO + var warehouse *warehouseDTO.WarehouseRelationDTO + + if len(purchase.Items) > 0 { + firstItem := purchase.Items[0] + if firstItem.TravelNumber != nil && strings.TrimSpace(*firstItem.TravelNumber) != "" { + travelNumber = *firstItem.TravelNumber + } + + if firstItem.Warehouse != nil && firstItem.Warehouse.Id != 0 { + mappedWarehouse := warehouseDTO.ToWarehouseRelationDTO(*firstItem.Warehouse) + warehouse = &mappedWarehouse + if firstItem.Warehouse.Area.Id != 0 { + mappedArea := areaDTO.ToAreaRelationDTO(firstItem.Warehouse.Area) + area = &mappedArea + } + } + + for _, item := range purchase.Items { + totalPrice += item.TotalPrice + } + } + + paymentPrice := paymentTotals[reference] + debtPrice := paymentPrice - totalPrice + + dueDate := "" + dueStatus := "-" + if purchase.DueDate != nil && !purchase.DueDate.IsZero() { + due := purchase.DueDate.In(loc) + dueDate = due.Format("2006-01-02") + if now.After(due) { + dueStatus = "Sudah Jatuh Tempo" + } else { + dueStatus = "Mendekati Jatuh Tempo" + } + } + + status := "Belum Lunas" + if debtPrice >= 0 { + status = "Lunas" + } + + poDate := "" + if purchase.PoDate != nil && !purchase.PoDate.IsZero() { + poDate = purchase.PoDate.In(loc).Format("2006-01-02") + } + + return dto.DebtSupplierRowDTO{ + PrNumber: prNumber, + PoNumber: poNumber, + PrDate: prDate.Format("2006-01-02"), + PoDate: poDate, + Aging: aging, + Area: area, + Warehouse: warehouse, + DueDate: dueDate, + DueStatus: dueStatus, + TotalPrice: totalPrice, + PaymentPrice: paymentPrice, + DebtPrice: debtPrice, + Status: status, + TravelNumber: travelNumber, + } +} + func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) { params, filters, err := s.parseHppPerKandangQuery(ctx) if err != nil { diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index b909d77c..6c80275f 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -43,6 +43,16 @@ type PurchaseSupplierQuery struct { FilterBy string `query:"filter_by" validate:"omitempty"` } +type DebtSupplierQuery struct { + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` + SupplierIDs []int64 `query:"-" validate:"omitempty,dive,gt=0"` + StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` + EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` + FilterBy string `query:"filter_by" validate:"omitempty,oneof=do_date po_date pr_date"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` +} + type HppPerKandangQuery struct { Page int `query:"page" validate:"omitempty,min=1,gt=0"` Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`