mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af2b3366ba | |||
| e015e20b5c |
@@ -15,13 +15,16 @@ import (
|
||||
|
||||
type DebtSupplierRepository interface {
|
||||
GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error)
|
||||
GetSuppliersWithDebts(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)
|
||||
GetExpensesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Expense, 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)
|
||||
GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error)
|
||||
GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]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)
|
||||
GetExpenseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
||||
}
|
||||
|
||||
type debtSupplierRepositoryImpl struct {
|
||||
@@ -490,3 +493,218 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Cont
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *debtSupplierRepositoryImpl) latestExpenseApproval(ctx context.Context) *gorm.DB {
|
||||
return r.db.WithContext(ctx).
|
||||
Table("approvals AS a").
|
||||
Select("a.approvable_id, a.step_number, a.action").
|
||||
Joins(`
|
||||
JOIN (
|
||||
SELECT approvable_id, MAX(action_at) AS latest_action_at
|
||||
FROM approvals
|
||||
WHERE approvable_type = ?
|
||||
GROUP BY approvable_id
|
||||
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
|
||||
string(utils.ApprovalWorkflowExpense),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *debtSupplierRepositoryImpl) baseExpenseSupplierIDs(ctx context.Context, filters *validation.DebtSupplierQuery) *gorm.DB {
|
||||
db := r.db.WithContext(ctx).
|
||||
Table("expenses").
|
||||
Select("DISTINCT expenses.supplier_id").
|
||||
Joins("JOIN (?) AS la ON la.approvable_id = expenses.id", r.latestExpenseApproval(ctx)).
|
||||
Where("la.step_number >= ?", uint16(utils.ExpenseStepFinance)).
|
||||
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
||||
Where("expenses.deleted_at IS NULL")
|
||||
|
||||
if len(filters.SupplierIDs) > 0 {
|
||||
db = db.Where("expenses.supplier_id IN ?", filters.SupplierIDs)
|
||||
}
|
||||
|
||||
if filters.AllowedLocationIDs != nil {
|
||||
if len(filters.AllowedLocationIDs) == 0 {
|
||||
db = db.Where("1 = 0")
|
||||
} else {
|
||||
db = db.Where("expenses.location_id IN ?", filters.AllowedLocationIDs)
|
||||
}
|
||||
}
|
||||
|
||||
if filters.AllowedAreaIDs != nil {
|
||||
if len(filters.AllowedAreaIDs) == 0 {
|
||||
db = db.Where("1 = 0")
|
||||
} else {
|
||||
db = db.Joins("JOIN locations exp_loc ON exp_loc.id = expenses.location_id").
|
||||
Where("exp_loc.area_id IN ?", filters.AllowedAreaIDs)
|
||||
}
|
||||
}
|
||||
|
||||
if filters.StartDate != "" {
|
||||
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
||||
db = db.Where("DATE(expenses.transaction_date) >= ?", dateFrom)
|
||||
}
|
||||
}
|
||||
|
||||
if filters.EndDate != "" {
|
||||
if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil {
|
||||
db = db.Where("DATE(expenses.transaction_date) <= ?", dateTo)
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (r *debtSupplierRepositoryImpl) GetSuppliersWithDebts(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error) {
|
||||
purchaseSubquery := r.baseSupplierQuery(ctx, filters).
|
||||
Select("suppliers.id")
|
||||
|
||||
expenseSubquery := r.baseExpenseSupplierIDs(ctx, filters)
|
||||
|
||||
db := r.db.WithContext(ctx).
|
||||
Model(&entity.Supplier{}).
|
||||
Where("suppliers.id IN (? UNION ?) AND suppliers.deleted_at IS NULL",
|
||||
purchaseSubquery, expenseSubquery)
|
||||
|
||||
var totalSuppliers int64
|
||||
if err := db.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
|
||||
}
|
||||
|
||||
type supplierIDResult struct {
|
||||
ID uint `gorm:"column:id"`
|
||||
Name string `gorm:"column:name"`
|
||||
}
|
||||
var idResults []supplierIDResult
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entity.Supplier{}).
|
||||
Where("suppliers.id IN (? UNION ?) AND suppliers.deleted_at IS NULL",
|
||||
purchaseSubquery, expenseSubquery).
|
||||
Select("suppliers.id, suppliers.name").
|
||||
Group("suppliers.id, suppliers.name").
|
||||
Order(resolveDebtSupplierSortClause(filters)).
|
||||
Offset(offset).
|
||||
Limit(limit).
|
||||
Scan(&idResults).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
supplierIDs := make([]uint, 0, len(idResults))
|
||||
for _, r := range idResults {
|
||||
supplierIDs = append(supplierIDs, r.ID)
|
||||
}
|
||||
if len(supplierIDs) == 0 {
|
||||
return []entity.Supplier{}, totalSuppliers, nil
|
||||
}
|
||||
|
||||
var suppliers []entity.Supplier
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id IN ?", supplierIDs).
|
||||
Order(resolveDebtSupplierSortClause(filters)).
|
||||
Find(&suppliers).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return suppliers, totalSuppliers, nil
|
||||
}
|
||||
|
||||
func (r *debtSupplierRepositoryImpl) GetExpensesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Expense, error) {
|
||||
if len(supplierIDs) == 0 {
|
||||
return []entity.Expense{}, nil
|
||||
}
|
||||
|
||||
db := r.db.WithContext(ctx).
|
||||
Model(&entity.Expense{}).
|
||||
Joins("JOIN (?) AS la ON la.approvable_id = expenses.id", r.latestExpenseApproval(ctx)).
|
||||
Where("expenses.supplier_id IN ?", supplierIDs).
|
||||
Where("la.step_number >= ?", uint16(utils.ExpenseStepFinance)).
|
||||
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
||||
Where("expenses.deleted_at IS NULL")
|
||||
|
||||
if filters.AllowedLocationIDs != nil {
|
||||
if len(filters.AllowedLocationIDs) == 0 {
|
||||
db = db.Where("1 = 0")
|
||||
} else {
|
||||
db = db.Where("expenses.location_id IN ?", filters.AllowedLocationIDs)
|
||||
}
|
||||
}
|
||||
|
||||
if filters.AllowedAreaIDs != nil {
|
||||
if len(filters.AllowedAreaIDs) == 0 {
|
||||
db = db.Where("1 = 0")
|
||||
} else {
|
||||
db = db.Joins("JOIN locations exp_loc ON exp_loc.id = expenses.location_id").
|
||||
Where("exp_loc.area_id IN ?", filters.AllowedAreaIDs)
|
||||
}
|
||||
}
|
||||
|
||||
if filters.StartDate != "" {
|
||||
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
||||
db = db.Where("DATE(expenses.transaction_date) >= ?", dateFrom)
|
||||
}
|
||||
}
|
||||
|
||||
if filters.EndDate != "" {
|
||||
if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil {
|
||||
db = db.Where("DATE(expenses.transaction_date) <= ?", dateTo)
|
||||
}
|
||||
}
|
||||
|
||||
var expenses []entity.Expense
|
||||
if err := db.
|
||||
Preload("Supplier").
|
||||
Preload("Nonstocks").
|
||||
Preload("Location").
|
||||
Preload("Location.Area").
|
||||
Order("expenses.transaction_date ASC, expenses.id ASC").
|
||||
Find(&expenses).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return expenses, nil
|
||||
}
|
||||
|
||||
func (r *debtSupplierRepositoryImpl) GetExpenseTotalsBeforeDate(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 expenseTotalRow struct {
|
||||
SupplierID uint `gorm:"column:supplier_id"`
|
||||
Total float64 `gorm:"column:total"`
|
||||
}
|
||||
|
||||
rows := make([]expenseTotalRow, 0)
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("expenses").
|
||||
Select("expenses.supplier_id AS supplier_id, SUM(en.qty * en.price) AS total").
|
||||
Joins("JOIN expense_nonstocks en ON en.expense_id = expenses.id").
|
||||
Joins("JOIN (?) AS la ON la.approvable_id = expenses.id", r.latestExpenseApproval(ctx)).
|
||||
Where("expenses.supplier_id IN ?", supplierIDs).
|
||||
Where("la.step_number >= ?", uint16(utils.ExpenseStepFinance)).
|
||||
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
|
||||
Where("expenses.deleted_at IS NULL").
|
||||
Where("DATE(expenses.transaction_date) < ?", dateFrom).
|
||||
Group("expenses.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
|
||||
}
|
||||
|
||||
@@ -1782,7 +1782,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
offset = 0
|
||||
}
|
||||
|
||||
suppliers, totalSuppliers, err := s.DebtSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params)
|
||||
suppliers, totalSuppliers, err := s.DebtSupplierRepo.GetSuppliersWithDebts(c.Context(), offset, params.Limit, params)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -1807,11 +1807,21 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
expenses, err := s.DebtSupplierRepo.GetExpensesBySuppliers(c.Context(), supplierIDs, params)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs))
|
||||
for _, purchase := range purchases {
|
||||
purchasesBySupplier[purchase.SupplierId] = append(purchasesBySupplier[purchase.SupplierId], purchase)
|
||||
}
|
||||
|
||||
expensesBySupplier := make(map[uint][]entity.Expense, len(supplierIDs))
|
||||
for _, exp := range expenses {
|
||||
expensesBySupplier[uint(exp.SupplierId)] = append(expensesBySupplier[uint(exp.SupplierId)], exp)
|
||||
}
|
||||
|
||||
paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs))
|
||||
for _, payment := range payments {
|
||||
paymentsBySupplier[payment.PartyId] = append(paymentsBySupplier[payment.PartyId], payment)
|
||||
@@ -1827,6 +1837,11 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
initialExpenseTotals, err := s.DebtSupplierRepo.GetExpenseTotalsBeforeDate(c.Context(), supplierIDs, params)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
initialBalanceTotals, err := s.DebtSupplierRepo.GetInitialBalanceTotals(c.Context(), supplierIDs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
@@ -1847,10 +1862,10 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
CountTotals bool
|
||||
}
|
||||
type debtSupplierAllocation struct {
|
||||
RowIndex int
|
||||
SortTime time.Time
|
||||
Amount float64
|
||||
Purchase entity.Purchase
|
||||
RowIndex int
|
||||
SortTime time.Time
|
||||
Amount float64
|
||||
CalcAging func(endDate time.Time) int
|
||||
}
|
||||
type paymentAllocation struct {
|
||||
Date time.Time
|
||||
@@ -1863,7 +1878,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
continue
|
||||
}
|
||||
|
||||
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID])
|
||||
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID] - initialExpenseTotals[supplierID])
|
||||
items := purchasesBySupplier[supplierID]
|
||||
paymentItems := paymentsBySupplier[supplierID]
|
||||
total := dto.DebtSupplierTotalDTO{}
|
||||
@@ -1881,11 +1896,32 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
DeltaBalance: -row.TotalPrice,
|
||||
CountTotals: true,
|
||||
})
|
||||
capturedPurchase := purchase
|
||||
purchaseAllocations = append(purchaseAllocations, debtSupplierAllocation{
|
||||
RowIndex: rowIndex,
|
||||
SortTime: sortTime,
|
||||
Amount: row.TotalPrice,
|
||||
Purchase: purchase,
|
||||
RowIndex: rowIndex,
|
||||
SortTime: sortTime,
|
||||
Amount: row.TotalPrice,
|
||||
CalcAging: func(endDate time.Time) int { return calculateDebtSupplierAging(capturedPurchase, endDate, location) },
|
||||
})
|
||||
}
|
||||
|
||||
for _, exp := range expensesBySupplier[supplierID] {
|
||||
row := buildDebtSupplierExpenseRow(exp, now, location)
|
||||
sortTime := exp.TransactionDate.In(location)
|
||||
rowIndex := len(combinedRows)
|
||||
combinedRows = append(combinedRows, debtSupplierRowItem{
|
||||
Row: row,
|
||||
SortTime: sortTime,
|
||||
Order: 0,
|
||||
DeltaBalance: -row.TotalPrice,
|
||||
CountTotals: true,
|
||||
})
|
||||
capturedExp := exp
|
||||
purchaseAllocations = append(purchaseAllocations, debtSupplierAllocation{
|
||||
RowIndex: rowIndex,
|
||||
SortTime: sortTime,
|
||||
Amount: row.TotalPrice,
|
||||
CalcAging: func(endDate time.Time) int { return calculateExpenseAging(capturedExp, endDate, location) },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1950,7 +1986,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
if remaining[purchaseIndex] <= 0.000001 {
|
||||
allocation := purchaseAllocations[purchaseIndex]
|
||||
combinedRows[allocation.RowIndex].Row.Status = "Lunas"
|
||||
combinedRows[allocation.RowIndex].Row.Aging = calculateDebtSupplierAging(allocation.Purchase, pay.Date, location)
|
||||
combinedRows[allocation.RowIndex].Row.Aging = allocation.CalcAging(pay.Date)
|
||||
purchaseIndex++
|
||||
}
|
||||
}
|
||||
@@ -2224,6 +2260,62 @@ func resolveDebtSupplierReceivedDate(purchase entity.Purchase, loc *time.Locatio
|
||||
return time.Date(earliest.Year(), earliest.Month(), earliest.Day(), 0, 0, 0, 0, loc)
|
||||
}
|
||||
|
||||
func buildDebtSupplierExpenseRow(exp entity.Expense, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO {
|
||||
txDate := exp.TransactionDate.In(loc)
|
||||
dateStr := txDate.Format("2006-01-02")
|
||||
|
||||
startDay := time.Date(txDate.Year(), txDate.Month(), txDate.Day(), 0, 0, 0, 0, loc)
|
||||
endDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
||||
aging := 0
|
||||
if !startDay.IsZero() && !endDay.Before(startDay) {
|
||||
aging = int(endDay.Sub(startDay).Hours() / 24)
|
||||
}
|
||||
|
||||
totalPrice := 0.0
|
||||
for _, ns := range exp.Nonstocks {
|
||||
totalPrice += ns.Qty * ns.Price
|
||||
}
|
||||
|
||||
var area *areaDTO.AreaRelationDTO
|
||||
if exp.Location != nil && exp.Location.Area.Id != 0 {
|
||||
mapped := areaDTO.ToAreaRelationDTO(exp.Location.Area)
|
||||
area = &mapped
|
||||
}
|
||||
|
||||
poNumber := ""
|
||||
if strings.TrimSpace(exp.PoNumber) != "" {
|
||||
poNumber = exp.PoNumber
|
||||
}
|
||||
|
||||
return dto.DebtSupplierRowDTO{
|
||||
PrNumber: exp.ReferenceNumber,
|
||||
PoNumber: poNumber,
|
||||
PoDate: dateStr,
|
||||
ReceivedDate: dateStr,
|
||||
Aging: aging,
|
||||
Area: area,
|
||||
Warehouse: nil,
|
||||
DueDate: "-",
|
||||
DueStatus: "-",
|
||||
TotalPrice: totalPrice,
|
||||
PaymentPrice: 0,
|
||||
DebtPrice: 0,
|
||||
Status: "Belum Lunas",
|
||||
TravelNumber: "-",
|
||||
Balance: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func calculateExpenseAging(exp entity.Expense, endDate time.Time, loc *time.Location) int {
|
||||
start := exp.TransactionDate.In(loc)
|
||||
startDay := time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, loc)
|
||||
stopDay := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, loc)
|
||||
if stopDay.Before(startDay) {
|
||||
return 0
|
||||
}
|
||||
return int(stopDay.Sub(startDay).Hours() / 24)
|
||||
}
|
||||
|
||||
func (s *repportService) GetHppV2Breakdown(ctx *fiber.Ctx, params *validation.HppV2BreakdownQuery) (*approvalService.HppV2Breakdown, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
|
||||
Reference in New Issue
Block a user