feat(BE):Rekapitulasi hutang supplier

This commit is contained in:
ragilap
2026-01-11 19:41:21 +07:00
parent d3c7d65bf5
commit 09f1b29359
12 changed files with 588 additions and 47 deletions
+22 -20
View File
@@ -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 (
+5 -11
View File
@@ -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)
}
+8 -8
View File
@@ -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)
}
@@ -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
}
@@ -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"`
}
+2 -1
View File
@@ -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)
@@ -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
}
+3 -2
View File
@@ -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)
}
@@ -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 {
@@ -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"`