Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4172fcea7 | ||
|
|
d5216e7994 | ||
|
|
b90a3cde4a | ||
|
|
7c8c7fb7db |
@@ -6,7 +6,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"apskel-pos-be/internal/entities"
|
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
"apskel-pos-be/internal/repository"
|
"apskel-pos-be/internal/repository"
|
||||||
)
|
)
|
||||||
@@ -453,15 +452,46 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
todayPromosi := getExpenseAmountByCategory(result.TodayExpenseByCategory, "promosi")
|
type categoryAmount struct {
|
||||||
todayLainLain := getExpenseAmountByCategory(result.TodayExpenseByCategory, "lain")
|
Name string
|
||||||
todayTotalOps := todayPromosi + todayLainLain
|
TodayAmt float64
|
||||||
todayGaji := getExpenseAmountByCategory(result.TodayExpenseByCategory, "gaji")
|
MtdAmt float64
|
||||||
|
}
|
||||||
|
|
||||||
mtdPromosi := getExpenseAmountByCategory(result.MtdExpenseByCategory, "promosi")
|
categoryMap := make(map[string]*categoryAmount)
|
||||||
mtdLainLain := getExpenseAmountByCategory(result.MtdExpenseByCategory, "lain")
|
var categoryOrder []string
|
||||||
mtdTotalOps := mtdPromosi + mtdLainLain
|
|
||||||
mtdGaji := getExpenseAmountByCategory(result.MtdExpenseByCategory, "gaji")
|
for _, cat := range result.TodayExpenseByCategory {
|
||||||
|
name := cat.CategoryName
|
||||||
|
if _, exists := categoryMap[name]; !exists {
|
||||||
|
categoryMap[name] = &categoryAmount{Name: name}
|
||||||
|
categoryOrder = append(categoryOrder, name)
|
||||||
|
}
|
||||||
|
categoryMap[name].TodayAmt = cat.Amount
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cat := range result.MtdExpenseByCategory {
|
||||||
|
name := cat.CategoryName
|
||||||
|
if _, exists := categoryMap[name]; !exists {
|
||||||
|
categoryMap[name] = &categoryAmount{Name: name}
|
||||||
|
categoryOrder = append(categoryOrder, name)
|
||||||
|
}
|
||||||
|
categoryMap[name].MtdAmt = cat.Amount
|
||||||
|
}
|
||||||
|
|
||||||
|
var todayTotalOps float64
|
||||||
|
var mtdTotalOps float64
|
||||||
|
var todayGaji float64
|
||||||
|
var mtdGaji float64
|
||||||
|
for _, cat := range categoryMap {
|
||||||
|
if isSalaryExpenseCategory(cat.Name) {
|
||||||
|
todayGaji += cat.TodayAmt
|
||||||
|
mtdGaji += cat.MtdAmt
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
todayTotalOps += cat.TodayAmt
|
||||||
|
mtdTotalOps += cat.MtdAmt
|
||||||
|
}
|
||||||
|
|
||||||
todayGrossProfit := result.TodayRevenue - result.TodayCost
|
todayGrossProfit := result.TodayRevenue - result.TodayCost
|
||||||
mtdGrossProfit := result.MtdRevenue - result.MtdCost
|
mtdGrossProfit := result.MtdRevenue - result.MtdCost
|
||||||
@@ -485,6 +515,33 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
|||||||
return (nominal / result.MtdRevenue) * 100
|
return (nominal / result.MtdRevenue) * 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opsSubItems := make([]models.ProfitLossSummaryRow, 0, len(categoryOrder)+1)
|
||||||
|
opsCategoryCount := 0
|
||||||
|
for _, name := range categoryOrder {
|
||||||
|
cat := categoryMap[name]
|
||||||
|
if isSalaryExpenseCategory(cat.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
opsCategoryCount++
|
||||||
|
opsSubItems = append(opsSubItems, models.ProfitLossSummaryRow{
|
||||||
|
ID: fmt.Sprintf("by_%s", slugify(name)),
|
||||||
|
Label: fmt.Sprintf("%d. %s", opsCategoryCount, cat.Name),
|
||||||
|
TodayNominal: cat.TodayAmt,
|
||||||
|
TodayPct: todayPct(cat.TodayAmt),
|
||||||
|
MtdNominal: cat.MtdAmt,
|
||||||
|
MtdPct: mtdPct(cat.MtdAmt),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
opsSubItems = append(opsSubItems, models.ProfitLossSummaryRow{
|
||||||
|
ID: "total_biaya_ops",
|
||||||
|
Label: fmt.Sprintf("Total Biaya OPS (%d kategori)", opsCategoryCount),
|
||||||
|
IsBold: true,
|
||||||
|
TodayNominal: todayTotalOps,
|
||||||
|
TodayPct: todayPct(todayTotalOps),
|
||||||
|
MtdNominal: mtdTotalOps,
|
||||||
|
MtdPct: mtdPct(mtdTotalOps),
|
||||||
|
})
|
||||||
|
|
||||||
mainSummary := []models.ProfitLossSummaryRow{
|
mainSummary := []models.ProfitLossSummaryRow{
|
||||||
{
|
{
|
||||||
ID: "total_omset", Label: "TOTAL OMSET",
|
ID: "total_omset", Label: "TOTAL OMSET",
|
||||||
@@ -505,23 +562,7 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
|||||||
ID: "biaya_ops", Label: "BIAYA OPS",
|
ID: "biaya_ops", Label: "BIAYA OPS",
|
||||||
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
|
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
|
||||||
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
|
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
|
||||||
SubItems: []models.ProfitLossSummaryRow{
|
SubItems: opsSubItems,
|
||||||
{
|
|
||||||
ID: "by_promosi", Label: "1. By Promosi",
|
|
||||||
TodayNominal: todayPromosi, TodayPct: todayPct(todayPromosi),
|
|
||||||
MtdNominal: mtdPromosi, MtdPct: mtdPct(mtdPromosi),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "by_lain_lain", Label: "2. By Lain lain",
|
|
||||||
TodayNominal: todayLainLain, TodayPct: todayPct(todayLainLain),
|
|
||||||
MtdNominal: mtdLainLain, MtdPct: mtdPct(mtdLainLain),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "total_biaya_ops", Label: "Total Biaya OPS (4.1+4.2)", IsBold: true,
|
|
||||||
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
|
|
||||||
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "laba_rugi_sblm_gaji", Label: "Laba/Rugi sblm Gaji (3-4)",
|
ID: "laba_rugi_sblm_gaji", Label: "Laba/Rugi sblm Gaji (3-4)",
|
||||||
@@ -577,11 +618,27 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExpenseAmountByCategory(categories []entities.ExpenseCategoryTotal, keyword string) float64 {
|
func isSalaryExpenseCategory(name string) bool {
|
||||||
for _, cat := range categories {
|
name = strings.ToLower(name)
|
||||||
if strings.Contains(strings.ToLower(cat.CategoryName), keyword) {
|
return strings.Contains(name, "gaji") || strings.Contains(name, "salary")
|
||||||
return cat.Amount
|
}
|
||||||
|
|
||||||
|
func slugify(s string) string {
|
||||||
|
result := make([]byte, 0, len(s))
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
c := s[i]
|
||||||
|
switch {
|
||||||
|
case c >= 'a' && c <= 'z':
|
||||||
|
result = append(result, c)
|
||||||
|
case c >= 'A' && c <= 'Z':
|
||||||
|
result = append(result, c+32)
|
||||||
|
case c >= '0' && c <= '9':
|
||||||
|
result = append(result, c)
|
||||||
|
default:
|
||||||
|
if len(result) == 0 || result[len(result)-1] != '_' {
|
||||||
|
result = append(result, '_')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0
|
}
|
||||||
|
return string(result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,3 +165,83 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *
|
|||||||
require.NotEmpty(t, result.MainSummary)
|
require.NotEmpty(t, result.MainSummary)
|
||||||
require.Equal(t, "total_omset", result.MainSummary[0].ID)
|
require.Equal(t, "total_omset", result.MainSummary[0].ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *testing.T) {
|
||||||
|
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
|
||||||
|
profitLossResult: &entities.ProfitLossAnalytics{
|
||||||
|
Summary: entities.ProfitLossSummary{
|
||||||
|
TotalRevenue: 10000,
|
||||||
|
TotalCost: 4000,
|
||||||
|
},
|
||||||
|
TodayRevenue: 10000,
|
||||||
|
TodayCost: 4000,
|
||||||
|
MtdRevenue: 20000,
|
||||||
|
MtdCost: 8000,
|
||||||
|
TodayExpenseByCategory: []entities.ExpenseCategoryTotal{
|
||||||
|
{CategoryName: "Gaji", Amount: 1500},
|
||||||
|
{CategoryName: "Promosi", Amount: 300},
|
||||||
|
{CategoryName: "Sewa", Amount: 500},
|
||||||
|
},
|
||||||
|
MtdExpenseByCategory: []entities.ExpenseCategoryTotal{
|
||||||
|
{CategoryName: "Gaji", Amount: 3000},
|
||||||
|
{CategoryName: "Promosi", Amount: 600},
|
||||||
|
{CategoryName: "Sewa", Amount: 1000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, expenseRepositoryStub{})
|
||||||
|
|
||||||
|
result, err := processor.GetProfitLossAnalytics(context.Background(), &models.ProfitLossAnalyticsRequest{
|
||||||
|
OrganizationID: uuid.New(),
|
||||||
|
DateFrom: now,
|
||||||
|
DateTo: now,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, result)
|
||||||
|
|
||||||
|
require.Len(t, result.MainSummary, 7)
|
||||||
|
|
||||||
|
require.Equal(t, "total_omset", result.MainSummary[0].ID)
|
||||||
|
require.Equal(t, float64(10000), result.MainSummary[0].TodayNominal)
|
||||||
|
require.Equal(t, float64(20000), result.MainSummary[0].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "hpp", result.MainSummary[1].ID)
|
||||||
|
require.Equal(t, float64(4000), result.MainSummary[1].TodayNominal)
|
||||||
|
require.Equal(t, float64(8000), result.MainSummary[1].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "laba_kotor", result.MainSummary[2].ID)
|
||||||
|
require.Equal(t, float64(6000), result.MainSummary[2].TodayNominal)
|
||||||
|
require.Equal(t, float64(12000), result.MainSummary[2].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "biaya_ops", result.MainSummary[3].ID)
|
||||||
|
require.Equal(t, float64(800), result.MainSummary[3].TodayNominal)
|
||||||
|
require.Equal(t, float64(1600), result.MainSummary[3].MtdNominal)
|
||||||
|
require.Len(t, result.MainSummary[3].SubItems, 3) // 2 operational categories + 1 total
|
||||||
|
|
||||||
|
require.Equal(t, "by_promosi", result.MainSummary[3].SubItems[0].ID)
|
||||||
|
require.Equal(t, float64(300), result.MainSummary[3].SubItems[0].TodayNominal)
|
||||||
|
require.Equal(t, float64(600), result.MainSummary[3].SubItems[0].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "by_sewa", result.MainSummary[3].SubItems[1].ID)
|
||||||
|
require.Equal(t, float64(500), result.MainSummary[3].SubItems[1].TodayNominal)
|
||||||
|
require.Equal(t, float64(1000), result.MainSummary[3].SubItems[1].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "total_biaya_ops", result.MainSummary[3].SubItems[2].ID)
|
||||||
|
require.True(t, result.MainSummary[3].SubItems[2].IsBold)
|
||||||
|
require.Equal(t, float64(800), result.MainSummary[3].SubItems[2].TodayNominal)
|
||||||
|
require.Equal(t, float64(1600), result.MainSummary[3].SubItems[2].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "laba_rugi_sblm_gaji", result.MainSummary[4].ID)
|
||||||
|
require.Equal(t, float64(5200), result.MainSummary[4].TodayNominal)
|
||||||
|
require.Equal(t, float64(10400), result.MainSummary[4].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "biaya_gaji", result.MainSummary[5].ID)
|
||||||
|
require.Equal(t, float64(1500), result.MainSummary[5].TodayNominal)
|
||||||
|
require.Equal(t, float64(3000), result.MainSummary[5].MtdNominal)
|
||||||
|
|
||||||
|
require.Equal(t, "laba_rugi", result.MainSummary[6].ID)
|
||||||
|
require.Equal(t, float64(3700), result.MainSummary[6].TodayNominal)
|
||||||
|
require.Equal(t, float64(7400), result.MainSummary[6].MtdNominal)
|
||||||
|
require.True(t, result.MainSummary[6].IsBold)
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ const (
|
|||||||
MetadataKeyLastSplitQuantities = "last_split_quantities"
|
MetadataKeyLastSplitQuantities = "last_split_quantities"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type SplitBillValidation struct {
|
type SplitBillValidation struct {
|
||||||
OrderItems map[uuid.UUID]*entities.OrderItem
|
OrderItems map[uuid.UUID]*entities.OrderItem
|
||||||
PaidQuantities map[uuid.UUID]int
|
PaidQuantities map[uuid.UUID]int
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ func (r *AnalyticsRepositoryImpl) GetSalesAnalytics(ctx context.Context, organiz
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.PurchasingAnalytics, error) {
|
func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.PurchasingAnalytics, error) {
|
||||||
var summary entities.PurchasingSummary
|
|
||||||
var outletName *string
|
var outletName *string
|
||||||
|
|
||||||
if outletID != nil {
|
if outletID != nil {
|
||||||
@@ -144,29 +143,32 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
outletName = &outlet.Name
|
outletName = &outlet.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return r.getPurchaseOrderPurchasingAnalytics(ctx, organizationID, outletID, outletName, dateFrom, dateTo, groupBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, outletName *string, dateFrom, dateTo time.Time, groupBy string) (*entities.PurchasingAnalytics, error) {
|
||||||
|
var summary entities.PurchasingSummary
|
||||||
summaryQuery := r.db.WithContext(ctx).
|
summaryQuery := r.db.WithContext(ctx).
|
||||||
Table("inventory_movements im").
|
Table("purchase_orders po").
|
||||||
Select(`
|
Select(`
|
||||||
COALESCE(SUM(im.total_cost), 0) as total_purchases,
|
COALESCE(SUM(poi.amount), 0) as total_purchases,
|
||||||
COUNT(DISTINCT im.reference_id) as total_purchase_orders,
|
COUNT(DISTINCT po.id) as total_purchase_orders,
|
||||||
COALESCE(SUM(im.quantity), 0) as total_quantity,
|
COALESCE(SUM(poi.quantity), 0) as total_quantity,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT(DISTINCT im.reference_id) > 0
|
WHEN COUNT(DISTINCT po.id) > 0
|
||||||
THEN COALESCE(SUM(im.total_cost), 0) / COUNT(DISTINCT im.reference_id)
|
THEN COALESCE(SUM(poi.amount), 0) / COUNT(DISTINCT po.id)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END as average_purchase_order_value,
|
END as average_purchase_order_value,
|
||||||
COUNT(DISTINCT im.item_id) as total_ingredients,
|
COUNT(DISTINCT i.id) as total_ingredients,
|
||||||
COUNT(DISTINCT po.vendor_id) as total_vendors
|
COUNT(DISTINCT po.vendor_id) as total_vendors
|
||||||
`).
|
`).
|
||||||
Joins("LEFT JOIN purchase_orders po ON im.reference_id = po.id").
|
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
|
||||||
Where("im.organization_id = ?", organizationID).
|
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||||
Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
|
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
||||||
Where("im.item_type = ?", "INGREDIENT").
|
Where("po.organization_id = ?", organizationID).
|
||||||
Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
|
Where("po.status != ?", "cancelled").
|
||||||
Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo)
|
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo)
|
||||||
|
summaryQuery = r.applyPurchaseOrderItemOutletFilter(summaryQuery, outletID)
|
||||||
summaryQuery = r.resolveOutletID(summaryQuery, outletID, "im.outlet_id")
|
|
||||||
|
|
||||||
if err := summaryQuery.Scan(&summary).Error; err != nil {
|
if err := summaryQuery.Scan(&summary).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -175,36 +177,35 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
var dateFormat string
|
var dateFormat string
|
||||||
switch groupBy {
|
switch groupBy {
|
||||||
case "hour":
|
case "hour":
|
||||||
dateFormat = "DATE_TRUNC('hour', im.created_at)"
|
dateFormat = "DATE_TRUNC('hour', po.created_at)"
|
||||||
case "week":
|
case "week":
|
||||||
dateFormat = "DATE_TRUNC('week', im.created_at)"
|
dateFormat = "DATE_TRUNC('week', po.transaction_date::timestamp)"
|
||||||
case "month":
|
case "month":
|
||||||
dateFormat = "DATE_TRUNC('month', im.created_at)"
|
dateFormat = "DATE_TRUNC('month', po.transaction_date::timestamp)"
|
||||||
default:
|
default:
|
||||||
dateFormat = "DATE_TRUNC('day', im.created_at)"
|
dateFormat = "DATE_TRUNC('day', po.transaction_date::timestamp)"
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []entities.PurchasingAnalyticsData
|
var data []entities.PurchasingAnalyticsData
|
||||||
dataQuery := r.db.WithContext(ctx).
|
dataQuery := r.db.WithContext(ctx).
|
||||||
Table("inventory_movements im").
|
Table("purchase_orders po").
|
||||||
Select(`
|
Select(`
|
||||||
`+dateFormat+` as date,
|
`+dateFormat+` as date,
|
||||||
COALESCE(SUM(im.total_cost), 0) as purchases,
|
COALESCE(SUM(poi.amount), 0) as purchases,
|
||||||
COUNT(DISTINCT im.reference_id) as purchase_orders,
|
COUNT(DISTINCT po.id) as purchase_orders,
|
||||||
COALESCE(SUM(im.quantity), 0) as quantity,
|
COALESCE(SUM(poi.quantity), 0) as quantity,
|
||||||
COUNT(DISTINCT im.item_id) as ingredients,
|
COUNT(DISTINCT i.id) as ingredients,
|
||||||
COUNT(DISTINCT po.vendor_id) as vendors
|
COUNT(DISTINCT po.vendor_id) as vendors
|
||||||
`).
|
`).
|
||||||
Joins("LEFT JOIN purchase_orders po ON im.reference_id = po.id").
|
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
|
||||||
Where("im.organization_id = ?", organizationID).
|
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||||
Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
|
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
||||||
Where("im.item_type = ?", "INGREDIENT").
|
Where("po.organization_id = ?", organizationID).
|
||||||
Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
|
Where("po.status != ?", "cancelled").
|
||||||
Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo).
|
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
|
||||||
Group(dateFormat).
|
Group(dateFormat).
|
||||||
Order(dateFormat)
|
Order(dateFormat)
|
||||||
|
dataQuery = r.applyPurchaseOrderItemOutletFilter(dataQuery, outletID)
|
||||||
dataQuery = r.resolveOutletID(dataQuery, outletID, "im.outlet_id")
|
|
||||||
|
|
||||||
if err := dataQuery.Scan(&data).Error; err != nil {
|
if err := dataQuery.Scan(&data).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -212,29 +213,28 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
|
|
||||||
var ingredientData []entities.PurchasingIngredientData
|
var ingredientData []entities.PurchasingIngredientData
|
||||||
ingredientQuery := r.db.WithContext(ctx).
|
ingredientQuery := r.db.WithContext(ctx).
|
||||||
Table("inventory_movements im").
|
Table("purchase_order_items poi").
|
||||||
Select(`
|
Select(`
|
||||||
i.id as ingredient_id,
|
i.id as ingredient_id,
|
||||||
i.name as ingredient_name,
|
i.name as ingredient_name,
|
||||||
COALESCE(SUM(im.quantity), 0) as quantity,
|
COALESCE(SUM(poi.quantity), 0) as quantity,
|
||||||
COALESCE(SUM(im.total_cost), 0) as total_cost,
|
COALESCE(SUM(poi.amount), 0) as total_cost,
|
||||||
CASE
|
CASE
|
||||||
WHEN SUM(im.quantity) > 0
|
WHEN SUM(poi.quantity) > 0
|
||||||
THEN COALESCE(SUM(im.total_cost), 0) / SUM(im.quantity)
|
THEN COALESCE(SUM(poi.amount), 0) / SUM(poi.quantity)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END as average_unit_cost,
|
END as average_unit_cost,
|
||||||
COUNT(DISTINCT im.reference_id) as purchase_order_count
|
COUNT(DISTINCT po.id) as purchase_order_count
|
||||||
`).
|
`).
|
||||||
Joins("JOIN ingredients i ON im.item_id = i.id").
|
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
|
||||||
Where("im.organization_id = ?", organizationID).
|
Joins("JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||||
Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
|
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
||||||
Where("im.item_type = ?", "INGREDIENT").
|
Where("po.organization_id = ?", organizationID).
|
||||||
Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
|
Where("po.status != ?", "cancelled").
|
||||||
Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo).
|
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
|
||||||
Group("i.id, i.name").
|
Group("i.id, i.name").
|
||||||
Order("total_cost DESC")
|
Order("total_cost DESC")
|
||||||
|
ingredientQuery = r.applyPurchaseOrderItemOutletFilter(ingredientQuery, outletID)
|
||||||
ingredientQuery = r.resolveOutletID(ingredientQuery, outletID, "im.outlet_id")
|
|
||||||
|
|
||||||
if err := ingredientQuery.Scan(&ingredientData).Error; err != nil {
|
if err := ingredientQuery.Scan(&ingredientData).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -242,26 +242,25 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
|
|
||||||
var vendorData []entities.PurchasingVendorData
|
var vendorData []entities.PurchasingVendorData
|
||||||
vendorQuery := r.db.WithContext(ctx).
|
vendorQuery := r.db.WithContext(ctx).
|
||||||
Table("inventory_movements im").
|
Table("purchase_orders po").
|
||||||
Select(`
|
Select(`
|
||||||
v.id as vendor_id,
|
v.id as vendor_id,
|
||||||
v.name as vendor_name,
|
v.name as vendor_name,
|
||||||
COALESCE(SUM(im.total_cost), 0) as total_cost,
|
COALESCE(SUM(poi.amount), 0) as total_cost,
|
||||||
COUNT(DISTINCT im.reference_id) as purchase_order_count,
|
COUNT(DISTINCT po.id) as purchase_order_count,
|
||||||
COUNT(DISTINCT im.item_id) as ingredient_count,
|
COUNT(DISTINCT i.id) as ingredient_count,
|
||||||
COALESCE(SUM(im.quantity), 0) as quantity
|
COALESCE(SUM(poi.quantity), 0) as quantity
|
||||||
`).
|
`).
|
||||||
Joins("JOIN purchase_orders po ON im.reference_id = po.id").
|
|
||||||
Joins("JOIN vendors v ON po.vendor_id = v.id").
|
Joins("JOIN vendors v ON po.vendor_id = v.id").
|
||||||
Where("im.organization_id = ?", organizationID).
|
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
|
||||||
Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
|
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||||
Where("im.item_type = ?", "INGREDIENT").
|
Joins("LEFT JOIN units u ON poi.unit_id = u.id").
|
||||||
Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
|
Where("po.organization_id = ?", organizationID).
|
||||||
Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo).
|
Where("po.status != ?", "cancelled").
|
||||||
|
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
|
||||||
Group("v.id, v.name").
|
Group("v.id, v.name").
|
||||||
Order("total_cost DESC")
|
Order("total_cost DESC")
|
||||||
|
vendorQuery = r.applyPurchaseOrderItemOutletFilter(vendorQuery, outletID)
|
||||||
vendorQuery = r.resolveOutletID(vendorQuery, outletID, "im.outlet_id")
|
|
||||||
|
|
||||||
if err := vendorQuery.Scan(&vendorData).Error; err != nil {
|
if err := vendorQuery.Scan(&vendorData).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -276,6 +275,13 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *AnalyticsRepositoryImpl) applyPurchaseOrderItemOutletFilter(query *gorm.DB, outletID *uuid.UUID) *gorm.DB {
|
||||||
|
if outletID == nil {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
return query.Where("(i.outlet_id = ? OR u.outlet_id = ?)", *outletID, *outletID)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) {
|
func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) {
|
||||||
var results []*entities.ProductAnalytics
|
var results []*entities.ProductAnalytics
|
||||||
|
|
||||||
@@ -637,7 +643,7 @@ func (r *AnalyticsRepositoryImpl) getExpenseByCategory(ctx context.Context, orga
|
|||||||
|
|
||||||
query := r.db.WithContext(ctx).
|
query := r.db.WithContext(ctx).
|
||||||
Table("expense_items ei").
|
Table("expense_items ei").
|
||||||
Select(`COALESCE(parent_coa.name, 'Lain-lain') as category_name, COALESCE(SUM(ei.amount), 0) as amount`).
|
Select(`COALESCE(parent_coa.name, coa.name, 'Lain-lain') as category_name, COALESCE(SUM(ei.amount), 0) as amount`).
|
||||||
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
||||||
Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
|
Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
|
||||||
Joins("LEFT JOIN chart_of_accounts parent_coa ON coa.parent_id = parent_coa.id").
|
Joins("LEFT JOIN chart_of_accounts parent_coa ON coa.parent_id = parent_coa.id").
|
||||||
@@ -650,8 +656,8 @@ func (r *AnalyticsRepositoryImpl) getExpenseByCategory(ctx context.Context, orga
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := query.
|
err := query.
|
||||||
Group("parent_coa.name").
|
Group("COALESCE(parent_coa.name, coa.name, 'Lain-lain')").
|
||||||
Order("parent_coa.name").
|
Order("COALESCE(parent_coa.name, coa.name, 'Lain-lain')").
|
||||||
Scan(&results).Error
|
Scan(&results).Error
|
||||||
|
|
||||||
return results, err
|
return results, err
|
||||||
|
|||||||
@@ -173,4 +173,3 @@ func (r *IngredientUnitConverterRepositoryImpl) ConvertQuantity(ctx context.Cont
|
|||||||
// If no converter found, return error
|
// If no converter found, return error
|
||||||
return 0, fmt.Errorf("no conversion found between units %s and %s for ingredient %s", fromUnitID, toUnitID, ingredientID)
|
return 0, fmt.Errorf("no conversion found between units %s and %s for ingredient %s", fromUnitID, toUnitID, ingredientID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user