mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'feat/BE/Rekapitulasi-hutang-supplier' into 'development'
[FIX/BE] adjustment response See merge request mbugroup/lti-api!162
This commit is contained in:
@@ -9,8 +9,8 @@ import (
|
||||
type DebtSupplierRowDTO struct {
|
||||
PrNumber string `json:"pr_number"`
|
||||
PoNumber string `json:"po_number"`
|
||||
PrDate string `json:"pr_date"`
|
||||
PoDate string `json:"po_date"`
|
||||
ReceivedDate string `json:"received_date"`
|
||||
Aging int `json:"aging"`
|
||||
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
||||
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
|
||||
@@ -21,6 +21,7 @@ type DebtSupplierRowDTO struct {
|
||||
DebtPrice float64 `json:"debt_price"`
|
||||
Status string `json:"status"`
|
||||
TravelNumber string `json:"travel_number"`
|
||||
Balance float64 `json:"balance"`
|
||||
}
|
||||
|
||||
type DebtSupplierTotalDTO struct {
|
||||
@@ -31,7 +32,8 @@ type DebtSupplierTotalDTO struct {
|
||||
}
|
||||
|
||||
type DebtSupplierDTO struct {
|
||||
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
|
||||
Rows []DebtSupplierRowDTO `json:"rows"`
|
||||
Total DebtSupplierTotalDTO `json:"total"`
|
||||
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
|
||||
InitialBalance float64 `json:"initial_balance"`
|
||||
Rows []DebtSupplierRowDTO `json:"rows"`
|
||||
Total DebtSupplierTotalDTO `json:"total"`
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ import (
|
||||
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)
|
||||
GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error)
|
||||
GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error)
|
||||
GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
||||
GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
||||
}
|
||||
|
||||
type debtSupplierRepositoryImpl struct {
|
||||
@@ -28,10 +31,10 @@ func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
|
||||
|
||||
func resolveDebtSupplierDateColumn(filterBy string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(filterBy)) {
|
||||
case "receive_date":
|
||||
return "purchases.receive_date"
|
||||
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:
|
||||
@@ -157,6 +160,39 @@ func (r *debtSupplierRepositoryImpl) GetPurchasesBySuppliers(ctx context.Context
|
||||
return purchases, nil
|
||||
}
|
||||
|
||||
func (r *debtSupplierRepositoryImpl) GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error) {
|
||||
if len(supplierIDs) == 0 {
|
||||
return []entity.Payment{}, nil
|
||||
}
|
||||
|
||||
db := r.db.WithContext(ctx).
|
||||
Model(&entity.Payment{}).
|
||||
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||
Where("direction = ?", "OUT").
|
||||
Where("party_id IN ?", supplierIDs)
|
||||
|
||||
if strings.TrimSpace(filters.StartDate) != "" {
|
||||
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
||||
db = db.Where("DATE(payment_date) >= ?", dateFrom)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(filters.EndDate) != "" {
|
||||
if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil {
|
||||
db = db.Where("DATE(payment_date) <= ?", dateTo)
|
||||
}
|
||||
}
|
||||
|
||||
var payments []entity.Payment
|
||||
if err := db.
|
||||
Order("payment_date ASC, id ASC").
|
||||
Find(&payments).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return payments, nil
|
||||
}
|
||||
|
||||
func (r *debtSupplierRepositoryImpl) getPurchaseIDs(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]uint, error) {
|
||||
dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy)
|
||||
|
||||
@@ -219,3 +255,76 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
|
||||
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
|
||||
return map[uint]float64{}, nil
|
||||
}
|
||||
|
||||
dateFrom, err := utils.ParseDateString(filters.StartDate)
|
||||
if err != nil {
|
||||
return map[uint]float64{}, nil
|
||||
}
|
||||
|
||||
dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy)
|
||||
|
||||
type purchaseTotalRow struct {
|
||||
SupplierID uint `gorm:"column:supplier_id"`
|
||||
Total float64 `gorm:"column:total"`
|
||||
}
|
||||
|
||||
rows := make([]purchaseTotalRow, 0)
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("purchases").
|
||||
Select("purchases.supplier_id AS supplier_id, SUM(purchase_items.total_price) AS total").
|
||||
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
|
||||
Where("purchases.supplier_id IN ?", supplierIDs).
|
||||
Where(fmt.Sprintf("DATE(%s) < ?", dateColumn), dateFrom).
|
||||
Group("purchases.supplier_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[uint]float64, len(rows))
|
||||
for _, row := range rows {
|
||||
result[row.SupplierID] = row.Total
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
|
||||
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
|
||||
return map[uint]float64{}, nil
|
||||
}
|
||||
|
||||
dateFrom, err := utils.ParseDateString(filters.StartDate)
|
||||
if err != nil {
|
||||
return map[uint]float64{}, nil
|
||||
}
|
||||
|
||||
type paymentTotalRow struct {
|
||||
SupplierID uint `gorm:"column:supplier_id"`
|
||||
Total float64 `gorm:"column:total"`
|
||||
}
|
||||
|
||||
rows := make([]paymentTotalRow, 0)
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entity.Payment{}).
|
||||
Select("party_id AS supplier_id, SUM(nominal) AS total").
|
||||
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||
Where("direction = ?", "OUT").
|
||||
Where("party_id IN ?", supplierIDs).
|
||||
Where("DATE(payment_date) < ?", dateFrom).
|
||||
Group("party_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[uint]float64, len(rows))
|
||||
for _, row := range rows {
|
||||
result[row.SupplierID] = row.Total
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -642,8 +642,8 @@ func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.Pu
|
||||
}
|
||||
|
||||
func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) {
|
||||
if params.FilterBy == "" {
|
||||
params.FilterBy = "do_date"
|
||||
if params.FilterBy == "" || strings.EqualFold(strings.TrimSpace(params.FilterBy), "do_date") {
|
||||
params.FilterBy = "received_date"
|
||||
}
|
||||
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
@@ -675,6 +675,11 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
payments, err := s.DebtSupplierRepo.GetPaymentsBySuppliers(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{})
|
||||
@@ -697,6 +702,21 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs))
|
||||
for _, payment := range payments {
|
||||
paymentsBySupplier[payment.PartyId] = append(paymentsBySupplier[payment.PartyId], payment)
|
||||
}
|
||||
|
||||
initialPurchaseTotals, err := s.DebtSupplierRepo.GetPurchaseTotalsBeforeDate(c.Context(), supplierIDs, params)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
initialPaymentTotals, err := s.DebtSupplierRepo.GetPaymentTotalsBeforeDate(c.Context(), supplierIDs, params)
|
||||
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")
|
||||
@@ -710,29 +730,81 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
continue
|
||||
}
|
||||
|
||||
initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
|
||||
|
||||
items := purchasesBySupplier[supplierID]
|
||||
rows := make([]dto.DebtSupplierRowDTO, 0, len(items))
|
||||
paymentItems := paymentsBySupplier[supplierID]
|
||||
rows := make([]dto.DebtSupplierRowDTO, 0, len(items)+len(paymentItems))
|
||||
total := dto.DebtSupplierTotalDTO{}
|
||||
|
||||
type debtSupplierRowItem struct {
|
||||
Row dto.DebtSupplierRowDTO
|
||||
SortTime time.Time
|
||||
Order int
|
||||
DeltaBalance float64
|
||||
CountTotals bool
|
||||
}
|
||||
|
||||
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
|
||||
for _, purchase := range items {
|
||||
row := buildDebtSupplierRow(purchase, paymentTotals, now, location)
|
||||
rows = append(rows, row)
|
||||
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
|
||||
combinedRows = append(combinedRows, debtSupplierRowItem{
|
||||
Row: row,
|
||||
SortTime: sortTime,
|
||||
Order: 0,
|
||||
DeltaBalance: -row.TotalPrice,
|
||||
CountTotals: true,
|
||||
})
|
||||
}
|
||||
|
||||
if row.Aging > total.Aging {
|
||||
total.Aging = row.Aging
|
||||
for _, payment := range paymentItems {
|
||||
row := buildDebtSupplierPaymentRow(payment, location)
|
||||
sortTime := payment.PaymentDate.In(location)
|
||||
combinedRows = append(combinedRows, debtSupplierRowItem{
|
||||
Row: row,
|
||||
SortTime: sortTime,
|
||||
Order: 1,
|
||||
DeltaBalance: payment.Nominal,
|
||||
CountTotals: false,
|
||||
})
|
||||
}
|
||||
|
||||
sort.SliceStable(combinedRows, func(i, j int) bool {
|
||||
if combinedRows[i].SortTime.Equal(combinedRows[j].SortTime) {
|
||||
return combinedRows[i].Order < combinedRows[j].Order
|
||||
}
|
||||
return combinedRows[i].SortTime.Before(combinedRows[j].SortTime)
|
||||
})
|
||||
|
||||
balance := initialBalance
|
||||
for i := range combinedRows {
|
||||
balance += combinedRows[i].DeltaBalance
|
||||
combinedRows[i].Row.Balance = balance
|
||||
|
||||
if combinedRows[i].CountTotals {
|
||||
row := combinedRows[i].Row
|
||||
if row.Aging > total.Aging {
|
||||
total.Aging = row.Aging
|
||||
}
|
||||
total.TotalPrice += row.TotalPrice
|
||||
total.PaymentPrice += row.PaymentPrice
|
||||
total.DebtPrice += row.DebtPrice
|
||||
} else {
|
||||
combinedRows[i].Row.DebtPrice = balance
|
||||
}
|
||||
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
|
||||
if sortDesc {
|
||||
for i := len(combinedRows) - 1; i >= 0; i-- {
|
||||
rows = append(rows, combinedRows[i].Row)
|
||||
}
|
||||
return rows[i].PrDate < rows[j].PrDate
|
||||
})
|
||||
} else {
|
||||
for i := range combinedRows {
|
||||
rows = append(rows, combinedRows[i].Row)
|
||||
}
|
||||
}
|
||||
|
||||
var supplierDTORef *supplierDTO.SupplierRelationDTO
|
||||
if supplier.Id != 0 {
|
||||
@@ -741,9 +813,10 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
}
|
||||
|
||||
result = append(result, dto.DebtSupplierDTO{
|
||||
Supplier: supplierDTORef,
|
||||
Rows: rows,
|
||||
Total: total,
|
||||
Supplier: supplierDTORef,
|
||||
InitialBalance: initialBalance,
|
||||
Rows: rows,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -769,6 +842,7 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
||||
|
||||
totalPrice := 0.0
|
||||
travelNumber := "-"
|
||||
receivedDate := ""
|
||||
var area *areaDTO.AreaRelationDTO
|
||||
var warehouse *warehouseDTO.WarehouseRelationDTO
|
||||
|
||||
@@ -787,8 +861,19 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
||||
}
|
||||
}
|
||||
|
||||
earliestReceived := time.Time{}
|
||||
for _, item := range purchase.Items {
|
||||
totalPrice += item.TotalPrice
|
||||
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
|
||||
continue
|
||||
}
|
||||
received := item.ReceivedDate.In(loc)
|
||||
if earliestReceived.IsZero() || received.Before(earliestReceived) {
|
||||
earliestReceived = received
|
||||
}
|
||||
}
|
||||
if !earliestReceived.IsZero() {
|
||||
receivedDate = earliestReceived.Format("2006-01-02")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,8 +905,8 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
||||
return dto.DebtSupplierRowDTO{
|
||||
PrNumber: prNumber,
|
||||
PoNumber: poNumber,
|
||||
PrDate: prDate.Format("2006-01-02"),
|
||||
PoDate: poDate,
|
||||
ReceivedDate: receivedDate,
|
||||
Aging: aging,
|
||||
Area: area,
|
||||
Warehouse: warehouse,
|
||||
@@ -835,6 +920,62 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
||||
}
|
||||
}
|
||||
|
||||
func buildDebtSupplierPaymentRow(payment entity.Payment, loc *time.Location) dto.DebtSupplierRowDTO {
|
||||
referenceNumber := ""
|
||||
if payment.ReferenceNumber != nil {
|
||||
referenceNumber = *payment.ReferenceNumber
|
||||
}
|
||||
|
||||
prNumber := payment.PaymentCode
|
||||
if strings.TrimSpace(prNumber) == "" {
|
||||
prNumber = referenceNumber
|
||||
}
|
||||
|
||||
return dto.DebtSupplierRowDTO{
|
||||
PrNumber: prNumber,
|
||||
PoNumber: referenceNumber,
|
||||
PoDate: "-",
|
||||
ReceivedDate: payment.PaymentDate.In(loc).Format("2006-01-02"),
|
||||
Aging: 0,
|
||||
Area: nil,
|
||||
Warehouse: nil,
|
||||
DueDate: "-",
|
||||
DueStatus: "-",
|
||||
TotalPrice: 0,
|
||||
PaymentPrice: payment.Nominal,
|
||||
DebtPrice: 0,
|
||||
Status: "Pembayaran",
|
||||
TravelNumber: "-",
|
||||
}
|
||||
}
|
||||
|
||||
func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc *time.Location) time.Time {
|
||||
switch strings.ToLower(strings.TrimSpace(filterBy)) {
|
||||
case "po_date":
|
||||
if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
|
||||
return purchase.PoDate.In(loc)
|
||||
}
|
||||
case "pr_date":
|
||||
return purchase.CreatedAt.In(loc)
|
||||
default:
|
||||
earliest := time.Time{}
|
||||
for _, item := range purchase.Items {
|
||||
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
|
||||
continue
|
||||
}
|
||||
received := item.ReceivedDate.In(loc)
|
||||
if earliest.IsZero() || received.Before(earliest) {
|
||||
earliest = received
|
||||
}
|
||||
}
|
||||
if !earliest.IsZero() {
|
||||
return earliest
|
||||
}
|
||||
}
|
||||
|
||||
return purchase.CreatedAt.In(loc)
|
||||
}
|
||||
|
||||
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
|
||||
params, filters, err := s.parseHppPerKandangQuery(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -49,7 +49,7 @@ type DebtSupplierQuery struct {
|
||||
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"`
|
||||
FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date pr_date do_date"`
|
||||
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user