Compare commits

..

9 Commits

Author SHA1 Message Date
giovanni af2b3366ba add vendor ekspedisi to laporan keuanga 2026-05-20 23:10:08 +07:00
Giovanni Gabriel Septriadi e015e20b5c Merge branch 'fix/expenses' into 'development'
[FIX][BE]: adjust response get report expense

See merge request mbugroup/lti-api!545
2026-05-20 07:40:50 +00:00
giovanni d92d28c892 adjust response get report expense 2026-05-20 14:39:36 +07:00
Giovanni Gabriel Septriadi 60bdd4a31a Merge branch 'feat/monitoring-saldo' into 'development'
add api monitoring saldo customer

See merge request mbugroup/lti-api!544
2026-05-20 03:35:50 +00:00
Giovanni Gabriel Septriadi cce0d44f83 Merge branch 'feat/lap-keuangan' into 'development'
add export customer payment control

See merge request mbugroup/lti-api!543
2026-05-20 01:49:58 +00:00
giovanni 6fc4ad5773 add api monitoring saldo customer 2026-05-19 18:42:57 +07:00
Giovanni Gabriel Septriadi e61625d2f7 Merge branch 'feat/lap-keuangan' into 'development'
[FEAT][BE]: add export laporan keuangan hutang ke supplier

See merge request mbugroup/lti-api!542
2026-05-19 11:41:05 +00:00
Giovanni Gabriel Septriadi 32c34be2c6 Merge branch 'fix/30' into 'development'
[FIX][BE]: fix status marketing when edit sales order and edit marketing delivery

See merge request mbugroup/lti-api!540
2026-05-19 05:16:40 +00:00
giovanni d2aa3ebac7 fix status marketing when edit sales order and edit marketing delivery 2026-05-19 12:15:53 +07:00
11 changed files with 1128 additions and 12 deletions
@@ -98,6 +98,7 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context
return db.
Preload("Expense").
Preload("Expense.Supplier").
Preload("Expense.Location").
Preload("Kandang").
Preload("Kandang.Location").
Preload("Nonstock").
@@ -542,9 +542,15 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
return nil, err
}
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
}
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
marketingRepoTx := marketingRepo.NewMarketingRepository(dbTransaction)
marketing, err := marketingRepoTx.GetByID(c.Context(), id, nil)
@@ -630,6 +636,23 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
}
}
if latestApproval != nil && latestApproval.StepNumber == uint16(utils.MarketingDeliveryOrder) {
action := entity.ApprovalActionUpdated
_, err := approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowMarketing,
id,
utils.MarketingStepSalesOrder,
&action,
actorID,
nil)
if err != nil {
if !errors.Is(err, gorm.ErrDuplicatedKey) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval to Sales Order")
}
}
}
return nil
})
if err != nil {
@@ -516,7 +516,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
c.Context(),
utils.ApprovalWorkflowMarketing,
id,
approvalutils.ApprovalStep(latestApproval.StepNumber),
utils.MarketingStepPengajuan,
&action,
actorID,
nil)
@@ -0,0 +1,71 @@
package dto
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
)
type BalanceMonitoringAyamDTO struct {
Ekor float64 `json:"ekor"`
Kg float64 `json:"kg"`
Nominal float64 `json:"nominal"`
}
type BalanceMonitoringTelurDTO struct {
Butir float64 `json:"butir"`
Kg float64 `json:"kg"`
Nominal float64 `json:"nominal"`
}
type BalanceMonitoringTradingDTO struct {
Qty float64 `json:"qty"`
Kg float64 `json:"kg"`
Nominal float64 `json:"nominal"`
}
type BalanceMonitoringRowDTO struct {
Customer customerDTO.CustomerRelationDTO `json:"customer"`
SaldoAwal float64 `json:"saldo_awal"`
PenjualanAyam BalanceMonitoringAyamDTO `json:"penjualan_ayam"`
PenjualanTelur BalanceMonitoringTelurDTO `json:"penjualan_telur"`
PenjualanTrading BalanceMonitoringTradingDTO `json:"penjualan_trading"`
Pembayaran float64 `json:"pembayaran"`
Aging int `json:"aging"`
AgingRataRata float64 `json:"aging_rata_rata"`
SaldoAkhir float64 `json:"saldo_akhir"`
}
type BalanceMonitoringTotalsDTO struct {
SaldoAwal float64 `json:"saldo_awal"`
PenjualanAyam BalanceMonitoringAyamDTO `json:"penjualan_ayam"`
PenjualanTelur BalanceMonitoringTelurDTO `json:"penjualan_telur"`
PenjualanTrading BalanceMonitoringTradingDTO `json:"penjualan_trading"`
Pembayaran float64 `json:"pembayaran"`
Aging int `json:"aging"`
AgingRataRata float64 `json:"aging_rata_rata"`
SaldoAkhir float64 `json:"saldo_akhir"`
}
func ToBalanceMonitoringRowDTO(
customer entity.Customer,
saldoAwal float64,
ayam BalanceMonitoringAyamDTO,
telur BalanceMonitoringTelurDTO,
trading BalanceMonitoringTradingDTO,
pembayaran float64,
aging int,
agingRataRata float64,
) BalanceMonitoringRowDTO {
saldoAkhir := saldoAwal + pembayaran - (ayam.Nominal + telur.Nominal + trading.Nominal)
return BalanceMonitoringRowDTO{
Customer: customerDTO.ToCustomerRelationDTO(customer),
SaldoAwal: saldoAwal,
PenjualanAyam: ayam,
PenjualanTelur: telur,
PenjualanTrading: trading,
Pembayaran: pembayaran,
Aging: aging,
AgingRataRata: agingRataRata,
SaldoAkhir: saldoAkhir,
}
}
@@ -6,6 +6,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
)
@@ -48,6 +49,7 @@ type RepportExpenseRealisasiDTO struct {
type RepportExpenseListDTO struct {
RepportExpenseBaseDTO
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
Pengajuan RepportExpensePengajuanDTO `json:"pengajuan"`
Realisasi RepportExpenseRealisasiDTO `json:"realisasi"`
@@ -133,6 +135,15 @@ func ToRepportExpenseListDTO(baseDTO RepportExpenseBaseDTO, ns *entity.ExpenseNo
totalRealisasi = ns.Realization.Qty * ns.Realization.Price
}
var location *locationDTO.LocationRelationDTO
if ns.Expense != nil && ns.Expense.Location != nil && ns.Expense.Location.Id != 0 {
mapped := locationDTO.ToLocationRelationDTO(*ns.Expense.Location)
location = &mapped
} else if ns.Kandang != nil && ns.Kandang.Location.Id != 0 {
mapped := locationDTO.ToLocationRelationDTO(ns.Kandang.Location)
location = &mapped
}
// Get kandang data at the main level
var kandang *kandangDTO.KandangRelationDTO
if ns.Kandang != nil && ns.Kandang.Id != 0 {
@@ -142,6 +153,7 @@ func ToRepportExpenseListDTO(baseDTO RepportExpenseBaseDTO, ns *entity.ExpenseNo
return RepportExpenseListDTO{
RepportExpenseBaseDTO: baseDTO,
Location: location,
Kandang: kandang,
Pengajuan: ToRepportExpensePengajuanDTO(ns),
Realisasi: realisasi,
+2
View File
@@ -40,6 +40,7 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
expenseDepreciationRepository := repportRepo.NewExpenseDepreciationRepository(db)
productionResultRepository := repportRepo.NewProductionResultRepository(db)
customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db)
balanceMonitoringRepository := repportRepo.NewBalanceMonitoringRepository(db)
customerRepository := customerRepo.NewCustomerRepository(db)
standardGrowthDetailRepository := productionStandardRepo.NewStandardGrowthDetailRepository(db)
productionStandardDetailRepository := productionStandardRepo.NewProductionStandardDetailRepository(db)
@@ -66,6 +67,7 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
hppPerKandangRepository,
productionResultRepository,
customerPaymentRepository,
balanceMonitoringRepository,
customerRepository,
standardGrowthDetailRepository,
productionStandardDetailRepository,
@@ -0,0 +1,518 @@
package repositories
import (
"context"
"fmt"
"strings"
"time"
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 BalanceMonitoringCategoryRow struct {
CustomerID uint `gorm:"column:customer_id"`
AyamQty float64 `gorm:"column:ayam_qty"`
AyamKg float64 `gorm:"column:ayam_kg"`
AyamNominal float64 `gorm:"column:ayam_nominal"`
TelurQty float64 `gorm:"column:telur_qty"`
TelurKg float64 `gorm:"column:telur_kg"`
TelurNominal float64 `gorm:"column:telur_nominal"`
TradingQty float64 `gorm:"column:trading_qty"`
TradingKg float64 `gorm:"column:trading_kg"`
TradingNominal float64 `gorm:"column:trading_nominal"`
}
type BalanceMonitoringAgingRow struct {
CustomerID uint `gorm:"column:customer_id"`
AgingMax int `gorm:"column:aging_max"`
AgingRataRata float64 `gorm:"column:aging_rata_rata"`
}
type BalanceMonitoringGrandTotalsRow struct {
SaldoAwalLifetime float64 `gorm:"column:saldo_awal_lifetime"`
SalesBeforeStart float64 `gorm:"column:sales_before_start"`
PaymentBeforeStart float64 `gorm:"column:payment_before_start"`
AyamQty float64 `gorm:"column:ayam_qty"`
AyamKg float64 `gorm:"column:ayam_kg"`
AyamNominal float64 `gorm:"column:ayam_nominal"`
TelurQty float64 `gorm:"column:telur_qty"`
TelurKg float64 `gorm:"column:telur_kg"`
TelurNominal float64 `gorm:"column:telur_nominal"`
TradingQty float64 `gorm:"column:trading_qty"`
TradingKg float64 `gorm:"column:trading_kg"`
TradingNominal float64 `gorm:"column:trading_nominal"`
PaymentInPeriod float64 `gorm:"column:payment_in_period"`
AgingMax int `gorm:"column:aging_max"`
AgingRataRata float64 `gorm:"column:aging_rata_rata"`
}
type BalanceMonitoringRepository interface {
GetCustomerIDsForBalanceMonitoring(ctx context.Context, offset, limit int, filters *validation.BalanceMonitoringQuery) ([]uint, int64, error)
GetAllFilteredCustomerIDs(ctx context.Context, filters *validation.BalanceMonitoringQuery) ([]uint, error)
GetSaldoAwalLifetime(ctx context.Context, customerIDs []uint) (map[uint]float64, error)
GetSalesTotalsBeforeDate(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]float64, error)
GetPaymentTotalsBeforeDate(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]float64, error)
GetSalesByCategoryInPeriod(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]BalanceMonitoringCategoryRow, error)
GetPaymentTotalsInPeriod(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]float64, error)
GetAgingPerCustomer(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]BalanceMonitoringAgingRow, error)
GetGrandTotals(ctx context.Context, filters *validation.BalanceMonitoringQuery) (BalanceMonitoringGrandTotalsRow, error)
}
type balanceMonitoringRepositoryImpl struct {
db *gorm.DB
}
func NewBalanceMonitoringRepository(db *gorm.DB) BalanceMonitoringRepository {
return &balanceMonitoringRepositoryImpl{db: db}
}
func resolveBalanceMonitoringDateColumn(filterBy string) string {
switch strings.ToLower(strings.TrimSpace(filterBy)) {
case "realized_at":
return "mdp.delivery_date"
case "sold_at", "":
return "m.so_date"
default:
return "m.so_date"
}
}
func resolveBalanceMonitoringDateRange(filters *validation.BalanceMonitoringQuery) (time.Time, time.Time, error) {
var startDate time.Time
var endDate time.Time
var err error
if strings.TrimSpace(filters.StartDate) != "" {
startDate, err = utils.ParseDateString(filters.StartDate)
if err != nil {
return time.Time{}, time.Time{}, err
}
} else {
startDate = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
if strings.TrimSpace(filters.EndDate) != "" {
endDate, err = utils.ParseDateString(filters.EndDate)
if err != nil {
return time.Time{}, time.Time{}, err
}
} else {
endDate = time.Now()
}
return startDate, endDate, nil
}
func resolveBalanceMonitoringSortClause(filters *validation.BalanceMonitoringQuery) string {
direction := "ASC"
if strings.EqualFold(strings.TrimSpace(filters.SortOrder), "desc") {
direction = "DESC"
}
switch strings.ToLower(strings.TrimSpace(filters.SortBy)) {
case "customer":
return "customers.name " + direction
default:
return "customers.name ASC"
}
}
func (r *balanceMonitoringRepositoryImpl) baseCustomerQuery(ctx context.Context, filters *validation.BalanceMonitoringQuery) *gorm.DB {
db := r.db.WithContext(ctx).
Model(&entity.Customer{}).
Where("customers.deleted_at IS NULL")
if len(filters.CustomerIDs) > 0 {
db = db.Where("customers.id IN ?", filters.CustomerIDs)
}
if len(filters.SalesIDs) > 0 {
db = db.Where("EXISTS (SELECT 1 FROM marketings m WHERE m.customer_id = customers.id AND m.deleted_at IS NULL AND m.sales_person_id IN ?)", filters.SalesIDs)
}
if filters.AllowedAreaIDs != nil || filters.AllowedLocationIDs != nil {
scopeSub := r.db.WithContext(ctx).
Table("marketings m").
Select("1").
Joins("JOIN marketing_products mp ON mp.marketing_id = m.id").
Joins("JOIN marketing_delivery_products mdp ON mdp.marketing_product_id = mp.id").
Joins("JOIN product_warehouses pw ON pw.id = mdp.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Where("m.customer_id = customers.id").
Where("m.deleted_at IS NULL").
Where("mdp.delivery_date IS NOT NULL")
if filters.AllowedAreaIDs != nil {
if len(filters.AllowedAreaIDs) == 0 {
db = db.Where("1 = 0")
} else {
scopeSub = scopeSub.Where("w.area_id IN ?", filters.AllowedAreaIDs)
}
}
if filters.AllowedLocationIDs != nil {
if len(filters.AllowedLocationIDs) == 0 {
db = db.Where("1 = 0")
} else {
scopeSub = scopeSub.Where("w.location_id IN ?", filters.AllowedLocationIDs)
}
}
db = db.Where("EXISTS (?)", scopeSub)
}
return db
}
func (r *balanceMonitoringRepositoryImpl) GetCustomerIDsForBalanceMonitoring(ctx context.Context, offset, limit int, filters *validation.BalanceMonitoringQuery) ([]uint, int64, error) {
var total int64
if err := r.baseCustomerQuery(ctx, filters).Count(&total).Error; err != nil {
return nil, 0, err
}
if total == 0 {
return []uint{}, 0, nil
}
if offset < 0 {
offset = 0
}
var customerIDs []uint
err := r.baseCustomerQuery(ctx, filters).
Order(resolveBalanceMonitoringSortClause(filters)).
Limit(limit).
Offset(offset).
Pluck("customers.id", &customerIDs).
Error
if err != nil {
return nil, 0, err
}
return customerIDs, total, nil
}
func (r *balanceMonitoringRepositoryImpl) GetAllFilteredCustomerIDs(ctx context.Context, filters *validation.BalanceMonitoringQuery) ([]uint, error) {
var customerIDs []uint
if err := r.baseCustomerQuery(ctx, filters).Pluck("customers.id", &customerIDs).Error; err != nil {
return nil, err
}
return customerIDs, nil
}
func (r *balanceMonitoringRepositoryImpl) GetSaldoAwalLifetime(ctx context.Context, customerIDs []uint) (map[uint]float64, error) {
if len(customerIDs) == 0 {
return map[uint]float64{}, nil
}
type row struct {
CustomerID uint `gorm:"column:customer_id"`
Total float64 `gorm:"column:total"`
}
rows := make([]row, 0)
err := r.db.WithContext(ctx).
Model(&entity.Payment{}).
Select("party_id AS customer_id, COALESCE(SUM(nominal), 0) AS total").
Where("party_type = ?", string(utils.PaymentPartyCustomer)).
Where("transaction_type = ?", string(utils.TransactionTypeSaldoAwal)).
Where("party_id IN ?", customerIDs).
Group("party_id").
Scan(&rows).Error
if err != nil {
return nil, err
}
result := make(map[uint]float64, len(rows))
for _, r := range rows {
result[r.CustomerID] = r.Total
}
return result, nil
}
func (r *balanceMonitoringRepositoryImpl) GetSalesTotalsBeforeDate(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]float64, error) {
if len(customerIDs) == 0 {
return map[uint]float64{}, nil
}
startDate, _, err := resolveBalanceMonitoringDateRange(filters)
if err != nil {
return map[uint]float64{}, nil
}
dateColumn := resolveBalanceMonitoringDateColumn(filters.FilterBy)
type row struct {
CustomerID uint `gorm:"column:customer_id"`
Total float64 `gorm:"column:total"`
}
rows := make([]row, 0)
db := r.db.WithContext(ctx).
Table("marketing_delivery_products mdp").
Select("m.customer_id AS customer_id, COALESCE(SUM(mdp.total_price), 0) AS total").
Joins("INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("INNER JOIN marketings m ON m.id = mp.marketing_id").
Where("m.customer_id IN ?", customerIDs).
Where("m.deleted_at IS NULL").
Where("mdp.delivery_date IS NOT NULL").
Where(fmt.Sprintf("DATE(%s) < ?", dateColumn), startDate)
if len(filters.SalesIDs) > 0 {
db = db.Where("m.sales_person_id IN ?", filters.SalesIDs)
}
if err := db.Group("m.customer_id").Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[uint]float64, len(rows))
for _, rr := range rows {
result[rr.CustomerID] = rr.Total
}
return result, nil
}
func (r *balanceMonitoringRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]float64, error) {
if len(customerIDs) == 0 {
return map[uint]float64{}, nil
}
startDate, _, err := resolveBalanceMonitoringDateRange(filters)
if err != nil {
return map[uint]float64{}, nil
}
type row struct {
CustomerID uint `gorm:"column:customer_id"`
Total float64 `gorm:"column:total"`
}
rows := make([]row, 0)
err = r.db.WithContext(ctx).
Model(&entity.Payment{}).
Select("party_id AS customer_id, COALESCE(SUM(nominal), 0) AS total").
Where("party_type = ?", string(utils.PaymentPartyCustomer)).
Where("transaction_type = ?", string(utils.TransactionTypePenjualan)).
Where("direction = ?", "IN").
Where("party_id IN ?", customerIDs).
Where("DATE(payment_date) < ?", startDate).
Group("party_id").
Scan(&rows).Error
if err != nil {
return nil, err
}
result := make(map[uint]float64, len(rows))
for _, rr := range rows {
result[rr.CustomerID] = rr.Total
}
return result, nil
}
func (r *balanceMonitoringRepositoryImpl) GetSalesByCategoryInPeriod(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]BalanceMonitoringCategoryRow, error) {
if len(customerIDs) == 0 {
return map[uint]BalanceMonitoringCategoryRow{}, nil
}
startDate, endDate, err := resolveBalanceMonitoringDateRange(filters)
if err != nil {
return map[uint]BalanceMonitoringCategoryRow{}, nil
}
dateColumn := resolveBalanceMonitoringDateColumn(filters.FilterBy)
rows := make([]BalanceMonitoringCategoryRow, 0)
db := r.db.WithContext(ctx).
Table("marketing_delivery_products mdp").
Select(`m.customer_id AS customer_id,
COALESCE(SUM(CASE WHEN m.marketing_type IN ('AYAM','AYAM_PULLET') THEN mdp.usage_qty ELSE 0 END), 0) AS ayam_qty,
COALESCE(SUM(CASE WHEN m.marketing_type IN ('AYAM','AYAM_PULLET') THEN mdp.total_weight ELSE 0 END), 0) AS ayam_kg,
COALESCE(SUM(CASE WHEN m.marketing_type IN ('AYAM','AYAM_PULLET') THEN mdp.total_price ELSE 0 END), 0) AS ayam_nominal,
COALESCE(SUM(CASE WHEN m.marketing_type = 'TELUR' THEN mdp.usage_qty ELSE 0 END), 0) AS telur_qty,
COALESCE(SUM(CASE WHEN m.marketing_type = 'TELUR' THEN mdp.total_weight ELSE 0 END), 0) AS telur_kg,
COALESCE(SUM(CASE WHEN m.marketing_type = 'TELUR' THEN mdp.total_price ELSE 0 END), 0) AS telur_nominal,
COALESCE(SUM(CASE WHEN m.marketing_type = 'TRADING' THEN mdp.usage_qty ELSE 0 END), 0) AS trading_qty,
COALESCE(SUM(CASE WHEN m.marketing_type = 'TRADING' THEN mdp.total_weight ELSE 0 END), 0) AS trading_kg,
COALESCE(SUM(CASE WHEN m.marketing_type = 'TRADING' THEN mdp.total_price ELSE 0 END), 0) AS trading_nominal`).
Joins("INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("INNER JOIN marketings m ON m.id = mp.marketing_id").
Where("m.customer_id IN ?", customerIDs).
Where("m.deleted_at IS NULL").
Where("mdp.delivery_date IS NOT NULL").
Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), startDate).
Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), endDate)
if len(filters.SalesIDs) > 0 {
db = db.Where("m.sales_person_id IN ?", filters.SalesIDs)
}
if err := db.Group("m.customer_id").Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[uint]BalanceMonitoringCategoryRow, len(rows))
for _, rr := range rows {
result[rr.CustomerID] = rr
}
return result, nil
}
func (r *balanceMonitoringRepositoryImpl) GetPaymentTotalsInPeriod(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]float64, error) {
if len(customerIDs) == 0 {
return map[uint]float64{}, nil
}
startDate, endDate, err := resolveBalanceMonitoringDateRange(filters)
if err != nil {
return map[uint]float64{}, nil
}
type row struct {
CustomerID uint `gorm:"column:customer_id"`
Total float64 `gorm:"column:total"`
}
rows := make([]row, 0)
err = r.db.WithContext(ctx).
Model(&entity.Payment{}).
Select("party_id AS customer_id, COALESCE(SUM(nominal), 0) AS total").
Where("party_type = ?", string(utils.PaymentPartyCustomer)).
Where("transaction_type = ?", string(utils.TransactionTypePenjualan)).
Where("direction = ?", "IN").
Where("party_id IN ?", customerIDs).
Where("DATE(payment_date) >= ?", startDate).
Where("DATE(payment_date) <= ?", endDate).
Group("party_id").
Scan(&rows).Error
if err != nil {
return nil, err
}
result := make(map[uint]float64, len(rows))
for _, rr := range rows {
result[rr.CustomerID] = rr.Total
}
return result, nil
}
func (r *balanceMonitoringRepositoryImpl) GetAgingPerCustomer(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]BalanceMonitoringAgingRow, error) {
if len(customerIDs) == 0 {
return map[uint]BalanceMonitoringAgingRow{}, nil
}
startDate, endDate, err := resolveBalanceMonitoringDateRange(filters)
if err != nil {
return map[uint]BalanceMonitoringAgingRow{}, nil
}
dateColumn := resolveBalanceMonitoringDateColumn(filters.FilterBy)
rows := make([]BalanceMonitoringAgingRow, 0)
db := r.db.WithContext(ctx).
Table("marketing_delivery_products mdp").
Select(`m.customer_id AS customer_id,
COALESCE(MAX(GREATEST(CURRENT_DATE - DATE(mdp.delivery_date), 0)), 0) AS aging_max,
COALESCE(
SUM(mdp.total_price * GREATEST(CURRENT_DATE - DATE(mdp.delivery_date), 0))::numeric
/ NULLIF(SUM(mdp.total_price), 0),
0
)::numeric(15,2) AS aging_rata_rata`).
Joins("INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("INNER JOIN marketings m ON m.id = mp.marketing_id").
Where("m.customer_id IN ?", customerIDs).
Where("m.deleted_at IS NULL").
Where("mdp.delivery_date IS NOT NULL").
Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), startDate).
Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), endDate)
if len(filters.SalesIDs) > 0 {
db = db.Where("m.sales_person_id IN ?", filters.SalesIDs)
}
if err := db.Group("m.customer_id").Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[uint]BalanceMonitoringAgingRow, len(rows))
for _, rr := range rows {
result[rr.CustomerID] = rr
}
return result, nil
}
func (r *balanceMonitoringRepositoryImpl) GetGrandTotals(ctx context.Context, filters *validation.BalanceMonitoringQuery) (BalanceMonitoringGrandTotalsRow, error) {
customerIDs, err := r.GetAllFilteredCustomerIDs(ctx, filters)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
if len(customerIDs) == 0 {
return BalanceMonitoringGrandTotalsRow{}, nil
}
saldoAwalLifetimeMap, err := r.GetSaldoAwalLifetime(ctx, customerIDs)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
salesBeforeMap, err := r.GetSalesTotalsBeforeDate(ctx, customerIDs, filters)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
paymentBeforeMap, err := r.GetPaymentTotalsBeforeDate(ctx, customerIDs, filters)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
categoryMap, err := r.GetSalesByCategoryInPeriod(ctx, customerIDs, filters)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
paymentInPeriodMap, err := r.GetPaymentTotalsInPeriod(ctx, customerIDs, filters)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
agingMap, err := r.GetAgingPerCustomer(ctx, customerIDs, filters)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
totals := BalanceMonitoringGrandTotalsRow{}
for _, total := range saldoAwalLifetimeMap {
totals.SaldoAwalLifetime += total
}
for _, total := range salesBeforeMap {
totals.SalesBeforeStart += total
}
for _, total := range paymentBeforeMap {
totals.PaymentBeforeStart += total
}
for _, cat := range categoryMap {
totals.AyamQty += cat.AyamQty
totals.AyamKg += cat.AyamKg
totals.AyamNominal += cat.AyamNominal
totals.TelurQty += cat.TelurQty
totals.TelurKg += cat.TelurKg
totals.TelurNominal += cat.TelurNominal
totals.TradingQty += cat.TradingQty
totals.TradingKg += cat.TradingKg
totals.TradingNominal += cat.TradingNominal
}
for _, total := range paymentInPeriodMap {
totals.PaymentInPeriod += total
}
for _, aging := range agingMap {
totals.AgingMax += aging.AgingMax
}
weightedSum := 0.0
weightTotal := 0.0
for cid, cat := range categoryMap {
nominal := cat.AyamNominal + cat.TelurNominal + cat.TradingNominal
if aging, ok := agingMap[cid]; ok && nominal > 0 {
weightedSum += nominal * aging.AgingRataRata
weightTotal += nominal
}
}
if weightTotal > 0 {
totals.AgingRataRata = weightedSum / weightTotal
}
return totals, nil
}
@@ -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
}
+1
View File
@@ -26,4 +26,5 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService
route.Get("/hpp-v2-breakdown", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppV2Breakdown)
route.Get("/production-result/:idProjectFlockKandang", m.RequirePermissions(m.P_ReportProductionResultGetAll), ctrl.GetProductionResult)
route.Get("/customer-payment", m.RequirePermissions(m.P_ReportCustomerPaymentGetAll), ctrl.GetCustomerPayment)
route.Get("/balance-monitoring", m.RequirePermissions(m.P_ReportCustomerPaymentGetAll), ctrl.GetBalanceMonitoring)
}
@@ -52,6 +52,7 @@ type RepportService interface {
GetHppV2Breakdown(ctx *fiber.Ctx, params *validation.HppV2BreakdownQuery) (*approvalService.HppV2Breakdown, error)
GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error)
GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error)
GetBalanceMonitoring(ctx *fiber.Ctx, params *validation.BalanceMonitoringQuery) ([]dto.BalanceMonitoringRowDTO, dto.BalanceMonitoringTotalsDTO, int64, error)
DB() *gorm.DB
}
@@ -74,6 +75,7 @@ type repportService struct {
HppPerKandangRepo repportRepo.HppPerKandangRepository
ProductionResultRepo repportRepo.ProductionResultRepository
CustomerPaymentRepo repportRepo.CustomerPaymentRepository
BalanceMonitoringRepo repportRepo.BalanceMonitoringRepository
CustomerRepo customerRepo.CustomerRepository
StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
@@ -106,6 +108,7 @@ func NewRepportService(
hppPerKandangRepo repportRepo.HppPerKandangRepository,
productionResultRepo repportRepo.ProductionResultRepository,
customerPaymentRepo repportRepo.CustomerPaymentRepository,
balanceMonitoringRepo repportRepo.BalanceMonitoringRepository,
customerRepo customerRepo.CustomerRepository,
standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository,
productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository,
@@ -129,6 +132,7 @@ func NewRepportService(
HppPerKandangRepo: hppPerKandangRepo,
ProductionResultRepo: productionResultRepo,
CustomerPaymentRepo: customerPaymentRepo,
BalanceMonitoringRepo: balanceMonitoringRepo,
CustomerRepo: customerRepo,
StandardGrowthDetailRepo: standardGrowthDetailRepo,
ProductionStandardDetailRepo: productionStandardDetailRepo,
@@ -1778,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
}
@@ -1803,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)
@@ -1823,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
@@ -1843,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
@@ -1859,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{}
@@ -1877,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) },
})
}
@@ -1946,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++
}
}
@@ -2220,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())
@@ -2893,3 +2989,163 @@ func parseOptionalFloat64(raw string) (*float64, error) {
return &value, nil
}
func (s *repportService) GetBalanceMonitoring(ctx *fiber.Ctx, params *validation.BalanceMonitoringQuery) ([]dto.BalanceMonitoringRowDTO, dto.BalanceMonitoringTotalsDTO, int64, error) {
if params.SortBy == "" {
params.SortBy = "customer"
}
if params.SortOrder == "" {
params.SortOrder = "asc"
}
if params.FilterBy == "" {
params.FilterBy = "sold_at"
}
if params.Page < 1 {
params.Page = 1
}
if params.Limit < 1 {
params.Limit = 10
}
if err := s.Validate.Struct(params); err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
locationScope, err := m.ResolveLocationScope(ctx, s.DB())
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
areaScope, err := m.ResolveAreaScope(ctx, s.DB())
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
if locationScope.Restrict {
params.AllowedLocationIDs = toInt64Slice(locationScope.IDs)
}
if areaScope.Restrict {
params.AllowedAreaIDs = toInt64Slice(areaScope.IDs)
}
offset := (params.Page - 1) * params.Limit
customerIDs, total, err := s.BalanceMonitoringRepo.GetCustomerIDsForBalanceMonitoring(ctx.Context(), offset, params.Limit, params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
if len(customerIDs) == 0 {
emptyTotals, gtErr := s.computeBalanceMonitoringTotals(ctx.Context(), params)
if gtErr != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, gtErr
}
return []dto.BalanceMonitoringRowDTO{}, emptyTotals, total, nil
}
saldoAwalLifetimeMap, err := s.BalanceMonitoringRepo.GetSaldoAwalLifetime(ctx.Context(), customerIDs)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
salesBeforeMap, err := s.BalanceMonitoringRepo.GetSalesTotalsBeforeDate(ctx.Context(), customerIDs, params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
paymentBeforeMap, err := s.BalanceMonitoringRepo.GetPaymentTotalsBeforeDate(ctx.Context(), customerIDs, params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
categoryMap, err := s.BalanceMonitoringRepo.GetSalesByCategoryInPeriod(ctx.Context(), customerIDs, params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
paymentInPeriodMap, err := s.BalanceMonitoringRepo.GetPaymentTotalsInPeriod(ctx.Context(), customerIDs, params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
agingMap, err := s.BalanceMonitoringRepo.GetAgingPerCustomer(ctx.Context(), customerIDs, params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
customers, err := s.CustomerRepo.GetByIDs(ctx.Context(), customerIDs, func(db *gorm.DB) *gorm.DB {
return db.Preload("Pic")
})
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
customerMap := make(map[uint]entity.Customer, len(customers))
for _, c := range customers {
customerMap[c.Id] = c
}
result := make([]dto.BalanceMonitoringRowDTO, 0, len(customerIDs))
for _, customerID := range customerIDs {
customer, ok := customerMap[customerID]
if !ok {
continue
}
saldoAwal := saldoAwalLifetimeMap[customerID] + paymentBeforeMap[customerID] - salesBeforeMap[customerID]
category := categoryMap[customerID]
ayam := dto.BalanceMonitoringAyamDTO{
Ekor: category.AyamQty,
Kg: category.AyamKg,
Nominal: category.AyamNominal,
}
telur := dto.BalanceMonitoringTelurDTO{
Butir: category.TelurQty,
Kg: category.TelurKg,
Nominal: category.TelurNominal,
}
trading := dto.BalanceMonitoringTradingDTO{
Qty: category.TradingQty,
Kg: category.TradingKg,
Nominal: category.TradingNominal,
}
pembayaran := paymentInPeriodMap[customerID]
aging := agingMap[customerID]
row := dto.ToBalanceMonitoringRowDTO(customer, saldoAwal, ayam, telur, trading, pembayaran, aging.AgingMax, aging.AgingRataRata)
result = append(result, row)
}
totals, err := s.computeBalanceMonitoringTotals(ctx.Context(), params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
return result, totals, total, nil
}
func (s *repportService) computeBalanceMonitoringTotals(ctx context.Context, params *validation.BalanceMonitoringQuery) (dto.BalanceMonitoringTotalsDTO, error) {
grand, err := s.BalanceMonitoringRepo.GetGrandTotals(ctx, params)
if err != nil {
return dto.BalanceMonitoringTotalsDTO{}, err
}
saldoAwal := grand.SaldoAwalLifetime + grand.PaymentBeforeStart - grand.SalesBeforeStart
saldoAkhir := saldoAwal + grand.PaymentInPeriod - (grand.AyamNominal + grand.TelurNominal + grand.TradingNominal)
return dto.BalanceMonitoringTotalsDTO{
SaldoAwal: saldoAwal,
PenjualanAyam: dto.BalanceMonitoringAyamDTO{
Ekor: grand.AyamQty,
Kg: grand.AyamKg,
Nominal: grand.AyamNominal,
},
PenjualanTelur: dto.BalanceMonitoringTelurDTO{
Butir: grand.TelurQty,
Kg: grand.TelurKg,
Nominal: grand.TelurNominal,
},
PenjualanTrading: dto.BalanceMonitoringTradingDTO{
Qty: grand.TradingQty,
Kg: grand.TradingKg,
Nominal: grand.TradingNominal,
},
Pembayaran: grand.PaymentInPeriod,
Aging: grand.AgingMax,
AgingRataRata: grand.AgingRataRata,
SaldoAkhir: saldoAkhir,
}, nil
}
@@ -116,3 +116,17 @@ type CustomerPaymentQuery struct {
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
}
type BalanceMonitoringQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,gt=0"`
CustomerIDs []uint `query:"-" validate:"omitempty,dive,gt=0"`
SalesIDs []uint `query:"-" validate:"omitempty,dive,gt=0"`
FilterBy string `query:"filter_by" validate:"omitempty,oneof=sold_at realized_at"`
SortBy string `query:"sort_by" validate:"omitempty,oneof=customer"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
AllowedAreaIDs []int64 `query:"-"`
AllowedLocationIDs []int64 `query:"-"`
}